diff --git a/.clang-tidy b/.clang-tidy index 9f99b26..5ac69ea 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -68,9 +68,18 @@ Checks: > -readability-redundant-access-specifiers # Scope to first-party code only: vendored headers under deps/ (httplib.h, -# json.hpp, nlohmann/) are NEVER linted. The filter matches both src/ and -# include/ under the signalwire-cpp tree. (CI is Linux; on macOS Apple libc++ +# json.hpp, nlohmann/) are NEVER linted. (CI is Linux; on macOS Apple libc++ # header drift can emit a trailing note — the header-filter keeps findings # scoped to our own headers regardless.) -HeaderFilterRegex: 'signalwire-cpp/(src|include)/' +# +# Widened 2026-07-30 alongside scripts/run-lint.sh, from (src|include) to every +# first-party tree, so headers pulled in from tools/, tests/ and the example +# trees are analysed the same way. tests/ matters especially: 123 test .cpp files +# are #included into test_main.cpp, so without tests/ here their findings would be +# filtered out as "not the main file" and the gate would be VACUOUS over them. deps/ remains excluded because we do not own it — and +# is now ALSO excluded at the compiler, via CMake marking it a SYSTEM include +# directory, which is the only lever that works for clang-diagnostic-* entries +# (those are compiler warnings surfaced through clang-tidy, so this regex never +# applies to them). +HeaderFilterRegex: 'signalwire-cpp/(src|include|tools|tests|examples|rest|relay)/' WarningsAsErrors: '*' diff --git a/.doc_surface_floor b/.doc_surface_floor index 03964c0..997b0a0 100644 --- a/.doc_surface_floor +++ b/.doc_surface_floor @@ -1 +1 @@ -90.2 +100.0 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 61a4c70..a67d82a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -108,18 +108,52 @@ jobs: with: python-version: '3.14' - - name: Install build deps (cmake + g++ + OpenSSL 3 headers) + - name: Install build deps (cmake + g++ + OpenSSL 3 headers + ccache) run: | sudo apt-get update - sudo apt-get install -y cmake g++ libssl-dev + sudo apt-get install -y cmake g++ libssl-dev ccache + + # ccache — COMPILER cache, persisted across runs. Distinct from ctcache below + # and non-overlapping: ctcache memoizes clang-tidy STATIC-ANALYSIS results, + # ccache memoizes COMPILER object output. This nightly job is where the two + # heaviest compilations live — the TEST gate's build/ and PACKAGE-SMOKE, which + # does a full Release build+install of the whole library into a scratch prefix + # and measured 13m07s on the 2026-08-05 nightly (run 30990423636, 08:53:51 -> + # 09:06:58). ccache was removed in 1fe7b2c because it "can't touch the LINT + # gate"; that is true of LINT and wrong for these builds, and with it absent + # every runner printed "note: ccache not found" and built fully cold. + # Measured locally (8 cores, 130 TUs, the PACKAGE-SMOKE Release shape): + # cold 344.7s -> warm 3.0s (130/130 direct hits) at an unchanged build path, + # 32.0s when only preprocessed-mode hits apply. NOTE the path caveat: + # package_smoke.py builds in a PID-unique sandbox (.sw-tmp/package-smoke-cpp-), + # so run-to-run it is the preprocessed-mode figure that applies, not the 3.0s one. + # CMakeLists.txt already availability-gates ccache as CMAKE_{C,CXX}_COMPILER_LAUNCHER. + # Both caches below key off a UTC date stamp; see the staleness note on the + # ctcache restore for why. Computed once here so the two agree exactly even + # across a UTC midnight boundary mid-job. + - name: Cache date stamp (rotates the restore-key daily) + id: cachedate + run: 'date -u +date=%Y-%m-%d >> "$GITHUB_OUTPUT"' + + # Same daily-rotated restore-key tier as ctcache below, for the same + # scope-before-recency reason (see the long note there). + - name: Restore compiler cache (ccache) + uses: actions/cache/restore@v6 + with: + path: ~/.cache/ccache + key: ccache-cpp-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + ccache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}- + ccache-cpp-${{ runner.os }}- # clang-tidy result cache (vendored ctcache, scripts/clang_tidy_cache.py). - # The LINT gate — clang-tidy over 65 TUs — is cpp's CI wall-clock driver - # (~824s serial, ~252s after the xargs fan-out). ccache does NOT help it - # (that's compilation; this is static analysis), which is why the old ccache - # cache-persistence here was dead weight — removed. Instead persist ctcache's - # per-TU result cache: an UNCHANGED TU (source, its headers, checks, config - # all identical) returns the cached result without running clang-tidy. + # The LINT gate — clang-tidy over 65 TUs — was cpp's CI wall-clock driver + # before this cache (~824s serial, ~252s after the xargs fan-out); it now + # completes in ~14s on this workflow. ccache does NOT help it (that's + # compilation; this is static analysis), which is why this SEPARATE cache + # exists. Persist ctcache's per-TU result cache: an UNCHANGED TU (source, its + # headers, checks, config all identical) returns the cached result without + # running clang-tidy. # Stable key + run_id suffix so each run restores the most recent cache and # saves an updated one; source-hashing the key would defeat the point (we # WANT it to persist across source changes and only miss on the changed TUs). @@ -128,6 +162,25 @@ jobs: # prior step, and every cpp run is currently red on the pre-existing # spec-coupling — so a combined cache would never populate. The separate # save step below runs with if: always(). + # + # STALENESS FIX (2026-08-05). Actions resolves restore-keys by SCOPE FIRST, + # recency second: a run sees its own ref's caches and the default branch's, + # and an own-ref match wins even when the main-scope one is far newer. So a + # long-lived branch pins itself to whatever entry it wrote long ago and never + # advances. Measured on THIS workflow, same gate, same branch: + # run 30908553939 (2026-08-04) restored ctcache-cpp-Linux-30465863814, + # written 2026-07-29 on refs/heads/wave6/ctor-dunder-fold -- SIX DAYS + # stale, chosen over a refs/heads/main entry from 3h earlier that was + # visible to it -- and LINT took 12m45s (12:35:14 -> 12:47:59). + # run 30990423636 (2026-08-05) restored a 1-day-old entry: LINT took 14s. + # 55x, entirely from which store got restored. Adding more bare-prefix + # fallbacks cannot fix it -- the own-ref scope is consulted before them. + # The fix is to make the stale entry UNMATCHABLE by the first-choice key: + # a date stamp ahead of the bare prefix, so today's runs agree on a key that + # an older branch entry cannot satisfy, falling through to the undated prefix + # only when no recent store exists. ctcache is content-addressed per TU, so + # restoring an older store is always CORRECT, just slower -- this changes + # which store is preferred, never the findings. - name: Restore clang-tidy cache (ctcache) id: ctcache_restore uses: actions/cache/restore@v6 @@ -135,6 +188,7 @@ jobs: path: ~/.cache/ctcache key: ctcache-cpp-${{ runner.os }}-${{ github.run_id }} restore-keys: | + ctcache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}- ctcache-cpp-${{ runner.os }}- # The FMT (clang-format) + LINT (clang-tidy) gates need the gate tools @@ -152,6 +206,15 @@ jobs: - name: Install pinned clang-format + clang-tidy 18.1.8 + clang-18 compiler run: | pip install "clang-format==18.1.8" "clang-tidy==18.1.8" + # ruff drives the PY-LINT gate (scripts/*.py). Declared here as well + # as in scripts/_env.sh so a CI runner has it, per AGENT_RULES §7. + # PINNED EXACT: an unbounded `pip install ruff` resolves the newest release + # at CI time, so a ruff release that adds a rule or changes a format + # heuristic reds FMT/LINT on a commit that was green locally. Keep in + # lockstep with SW_RUFF_VERSION in scripts/_env.sh. + pip install "ruff==0.15.21" + ruff --version + ruff --version | grep -qw '0\.15\.21' || { echo "FATAL: ruff is not the pinned 0.15.21" >&2; exit 1; } sudo apt-get install -y clang-18 sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-18 100 sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 100 @@ -214,17 +277,42 @@ jobs: # here (not in env:) so $HOME expands to an ABSOLUTE path — ctcache does # NOT expanduser CTCACHE_DIR, so a literal "~" would create a bogus dir. export CTCACHE_DIR="$HOME/.cache/ctcache" + # ccache's compiler cache. ~/.cache/ccache is already ccache's default, + # but set it EXPLICITLY so the dir the restore/save steps name can never + # drift from the dir ccache actually uses (e.g. if a future runner image + # ships an XDG_CACHE_HOME or a /etc/ccache.conf that relocates it — then + # we would be caching an empty dir and silently getting cold builds while + # the workflow still looked correct). Same absolute-path reason as above. + export CCACHE_DIR="$HOME/.cache/ccache" + # NOTE: CCACHE_BASEDIR/CCACHE_NOHASHDIR (which make ccache path-insensitive + # so PACKAGE-SMOKE's PID-unique sandbox can still hit direct mode — this + # workflow's largest gate at 13m07s, one full cold Release build+install) are + # set in scripts/_env.sh, NOT here — deliberately, so a local run and a CI run + # get the SAME cache behaviour. Setting them here off $GITHUB_WORKSPACE would + # apply on a runner only, and local devs would silently get preprocessed-only + # hits. _env.sh keys off $REPO, which it resolves CWD-independently. bash scripts/run-ci.sh # Save the ctcache store even if the gate failed (the LINT gate still ran + - # populated it). Unique run_id key so every run persists an updated cache; - # the next run's restore-keys prefix picks up the most recent. + # populated it). The saved key carries the SAME date stamp the restore-keys + # look for, so a later run today prefers a store written today over an older + # branch entry; run_id keeps every save unique so concurrent runs never + # collide on one key. - name: Save clang-tidy cache (ctcache) if: always() uses: actions/cache/save@v6 with: path: ~/.cache/ctcache - key: ctcache-cpp-${{ runner.os }}-${{ github.run_id }} + key: ctcache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}-${{ github.run_id }} + + # Save the compiler cache on the same if: always() terms and for the same + # reason — the builds ran and populated it even when a later gate went red. + - name: Save compiler cache (ccache) + if: always() + uses: actions/cache/save@v6 + with: + path: ~/.cache/ccache + key: ccache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}-${{ github.run_id }} - name: Record green marker for this input triple run: 'echo green > .nightly-green-marker' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 916398c..500218a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,16 +58,49 @@ jobs: with: python-version: '3.14' - - name: Install build deps (cmake + g++ + OpenSSL 3 headers) + - name: Install build deps (cmake + g++ + OpenSSL 3 headers + ccache) run: | sudo apt-get update - sudo apt-get install -y cmake g++ libssl-dev + sudo apt-get install -y cmake g++ libssl-dev ccache + + # ccache — COMPILER cache, persisted across runs. This is a different cache + # from ctcache below and they do NOT overlap: ctcache memoizes clang-tidy + # STATIC-ANALYSIS results, ccache memoizes COMPILER object output. ccache was + # removed from this workflow in 1fe7b2c on the reasoning that it "can't touch + # the LINT gate" — true, but that overshot: this job ALSO runs two real + # compilations (the TEST gate's build/ and, on the nightly tier, PACKAGE-SMOKE's + # full Release build+install of the whole library into a scratch prefix), and + # those are exactly what a compiler cache accelerates. With ccache absent every + # runner printed "note: ccache not found" and both builds were fully cold. + # Measured locally (8 cores, 130 TUs, Release build+install — the PACKAGE-SMOKE + # shape): cold 344.7s -> warm 3.0s (130/130 direct hits) when the build path is + # unchanged, and 32.0s when only the preprocessed-mode hits apply. CMakeLists.txt + # already availability-gates ccache as CMAKE_{C,CXX}_COMPILER_LAUNCHER, so its + # presence needs no build-system change and its absence stays a strict no-op. + # Same split restore/save + run_id-suffixed key rationale as ctcache below. + # Both caches below key off a UTC date stamp; see the staleness note on the + # ctcache restore for why. Computed once here so the two agree exactly even + # across a UTC midnight boundary mid-job. + - name: Cache date stamp (rotates the restore-key daily) + id: cachedate + run: 'date -u +date=%Y-%m-%d >> "$GITHUB_OUTPUT"' + + # Same daily-rotated restore-key tier as ctcache below, for the same + # scope-before-recency reason (see the long note there). + - name: Restore compiler cache (ccache) + uses: actions/cache/restore@v6 + with: + path: ~/.cache/ccache + key: ccache-cpp-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + ccache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}- + ccache-cpp-${{ runner.os }}- # clang-tidy result cache (vendored ctcache, scripts/clang_tidy_cache.py). - # The LINT gate — clang-tidy over 65 TUs — is cpp's CI wall-clock driver - # (~824s serial, ~252s after the xargs fan-out). ccache does NOT help it - # (that's compilation; this is static analysis), which is why the old ccache - # cache-persistence here was dead weight — removed. Instead persist ctcache's + # The LINT gate — clang-tidy over 65 TUs — was cpp's CI wall-clock driver + # before this cache (~824s serial, ~252s after the xargs fan-out); it now + # completes in ~14-27s. ccache does NOT help it (that's compilation; this is + # static analysis), which is why this SEPARATE cache exists. Persist ctcache's # per-TU result cache: an UNCHANGED TU (source, its headers, checks, config # all identical) returns the cached result without running clang-tidy. # Stable key + run_id suffix so each run restores the most recent cache and @@ -78,6 +111,25 @@ jobs: # prior step, and every cpp run is currently red on the pre-existing # spec-coupling — so a combined cache would never populate. The separate # save step below runs with if: always(). + # + # STALENESS FIX (2026-08-05). Actions resolves restore-keys by SCOPE FIRST, + # recency second: a run sees its own ref's caches and the default branch's, + # and an own-ref match wins even when the main-scope one is far newer. So a + # long-lived branch pins itself to whatever entry it wrote long ago and never + # advances. Measured on this repo, both nightly, same gate, same branch: + # run 30908553939 (2026-08-04) restored ctcache-cpp-Linux-30465863814, + # written 2026-07-29 on refs/heads/wave6/ctor-dunder-fold -- SIX DAYS + # stale, chosen over a refs/heads/main entry from 3h earlier that was + # visible to it -- and LINT took 12m45s (12:35:14 -> 12:47:59). + # run 30990423636 (2026-08-05) restored a 1-day-old entry: LINT took 14s. + # 55x, entirely from which store got restored. Adding more bare-prefix + # fallbacks cannot fix it -- the own-ref scope is consulted before them. + # The fix is to make the stale entry UNMATCHABLE by the first-choice key: + # a date stamp ahead of the bare prefix, so today's runs agree on a key that + # yesterday's stale branch entry cannot satisfy, and only fall through to the + # undated prefix when no recent store exists at all. ctcache is content- + # addressed per TU, so restoring an older store is always CORRECT, just + # slower -- this only changes which store is preferred, never the findings. - name: Restore clang-tidy cache (ctcache) id: ctcache_restore uses: actions/cache/restore@v6 @@ -85,6 +137,7 @@ jobs: path: ~/.cache/ctcache key: ctcache-cpp-${{ runner.os }}-${{ github.run_id }} restore-keys: | + ctcache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}- ctcache-cpp-${{ runner.os }}- # The FMT (clang-format) + LINT (clang-tidy) gates need the gate tools @@ -102,6 +155,15 @@ jobs: - name: Install pinned clang-format + clang-tidy 18.1.8 + clang-18 compiler run: | pip install "clang-format==18.1.8" "clang-tidy==18.1.8" + # ruff drives the PY-LINT gate (scripts/*.py). Declared here as well + # as in scripts/_env.sh so a CI runner has it, per AGENT_RULES §7. + # PINNED EXACT: an unbounded `pip install ruff` resolves the newest release + # at CI time, so a ruff release that adds a rule or changes a format + # heuristic reds FMT/LINT on a commit that was green locally. Keep in + # lockstep with SW_RUFF_VERSION in scripts/_env.sh. + pip install "ruff==0.15.21" + ruff --version + ruff --version | grep -qw '0\.15\.21' || { echo "FATAL: ruff is not the pinned 0.15.21" >&2; exit 1; } sudo apt-get install -y clang-18 sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-18 100 sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 100 @@ -180,14 +242,42 @@ jobs: # here (not in env:) so $HOME expands to an ABSOLUTE path — ctcache does # NOT expanduser CTCACHE_DIR, so a literal "~" would create a bogus dir. export CTCACHE_DIR="$HOME/.cache/ctcache" + # ccache's compiler cache. ~/.cache/ccache is already ccache's default, + # but set it EXPLICITLY so the dir the restore/save steps name can never + # drift from the dir ccache actually uses (e.g. if a future runner image + # ships an XDG_CACHE_HOME or a /etc/ccache.conf that relocates it — then + # we would be caching an empty dir and silently getting cold builds while + # the workflow still looked correct). Same absolute-path reason as above. + export CCACHE_DIR="$HOME/.cache/ccache" + # NOTE: CCACHE_BASEDIR/CCACHE_NOHASHDIR (which make ccache path-insensitive + # so PACKAGE-SMOKE's PID-unique sandbox can still hit direct mode) are set in + # scripts/_env.sh, NOT here — deliberately, so a local run and a CI run get + # the SAME cache behaviour. Setting them here off $GITHUB_WORKSPACE would + # apply on a runner only, and local devs would silently get preprocessed-only + # hits. _env.sh keys off $REPO, which it resolves CWD-independently. bash scripts/run-ci.sh # Save the ctcache store even if the gate failed (the LINT gate still ran + # populated it). Unique run_id key so every run persists an updated cache; # the next run's restore-keys prefix picks up the most recent. + # The saved key carries the SAME date stamp the restore-keys look for, so a + # later run TODAY prefers a store written today over any older branch entry. + # A run tomorrow finds no d match and falls through to the undated + # prefix -- the old behaviour -- so the worst case is exactly what we had + # before and the common case is a same-day store. run_id keeps every save + # unique so two concurrent runs never collide on one key. - name: Save clang-tidy cache (ctcache) if: always() uses: actions/cache/save@v6 with: path: ~/.cache/ctcache - key: ctcache-cpp-${{ runner.os }}-${{ github.run_id }} + key: ctcache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}-${{ github.run_id }} + + # Save the compiler cache on the same if: always() terms and for the same + # reason — the builds ran and populated it even when a later gate went red. + - name: Save compiler cache (ccache) + if: always() + uses: actions/cache/save@v6 + with: + path: ~/.cache/ccache + key: ccache-cpp-${{ runner.os }}-d${{ steps.cachedate.outputs.date }}-${{ github.run_id }} diff --git a/.version-frozen b/.version-frozen new file mode 100644 index 0000000..c0bc76a --- /dev/null +++ b/.version-frozen @@ -0,0 +1,38 @@ +# VERSION FREEZE — TEMPORARY. DELETE THIS FILE BEFORE RELEASING. +# +# Ruled by the owner 2026-07-28. This port declares version 3.0.0, and its SemVer +# release floor (port_signatures.baseline.json -> baseline_version) is pinned at +# 3.0.0 with the recorded surface payload overwritten to match today's surface. +# +# THIS FILE IS DOCUMENTATION ONLY (owner ruling, 2026-07-28). No gate reads it and +# nothing fails if it is present. It exists so that neither a person nor a future +# session has to re-derive why the versions look the way they do — and so nobody +# casually bumps one. +# +# WHY THE FREEZE: versions had drifted to 3.0.2 / 3.2.0 / 3.2.1 / 3.3.0 / 4.0.0 +# across the fleet with NOTHING EVER PUBLISHED above v2.x. `git ls-remote --tags +# origin` tops out at v1.1.2 for rust/dotnet and v2.0.x for most others; php's +# v3.2.0 tag is local-only, zero remote matches. That scatter was never a release +# history, so collapsing it rewrote nothing real. +# +# WHILE THIS FILE EXISTS: +# - do NOT bump this port's version +# - do NOT re-anchor its release floor +# - DO land surface changes freely. The floor is a snapshot of the 3.0.0 wave, +# not of a published release, so SEMVER-DIFF will not flag anything already in +# today's surface. Anything added AFTER the freeze is still caught normally. +# +# BEFORE CUTTING A RELEASE: +# 1. DELETE this file in every port. +# 2. Re-anchor each port_signatures.baseline.json to the actual released +# commit/tag and regenerate its surface payload from that point. +# 3. Re-enable enforcement: ports invoke SEMVER-DIFF with --report-only today +# (the D5 "re-anchor at cut" setting). Dropping that flag turns semver +# enforcement back on. +# +# NOTE: perl is deliberately NOT floor-pinned. It is the only port anchored to a +# genuinely published tag (v2.0.2, sha 83376ff) and already passes at 3.0.0; +# overwriting it would destroy the fleet's only real release anchor. +# +# Owner rulings: porting-sdk tasks #143 (version) and #146 (floor). +# Full context: porting-sdk/SESSION_HANDOFF_2026-07-28.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b393dec..df63813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ All notable changes to the SignalWire AI Agents SDK for C++ are documented in this file. This project adheres to [Semantic Versioning](https://semver.org). -## [3.2.1] - 2026-07-15 +## [3.0.0] + +Parity release aligning the C++ SDK with the Python reference SDK across the +REST, RELAY, SWML, and SWAIG surfaces. The public API is generated from, and +continuously verified against, the shared SignalWire wire specification. ### Fixed - `datetime` skill: `get_current_time` / `get_current_date` now compute the time @@ -13,8 +17,6 @@ this file. This project adheres to [Semantic Versioning](https://semver.org). unknown/invalid zone returns an error result rather than a UTC answer labelled as that zone. The tool interface (name/params/required) is unchanged. -## [3.2.0] - 2026-07-15 - ### Added - `Messages` REST resource (`client.messages()`): send (`create`, POST `/api/messaging/messages`) and redact (`update`, PATCH @@ -23,21 +25,11 @@ this file. This project adheres to [Semantic Versioning](https://semver.org). shared mock server. Distinct from the message-logs endpoints exposed at `client.logs().messages`. -## [3.1.0] - 2026-07-14 - -### Added - `Projects` REST resource (`client.projects()`): full CRUD over `/api/projects` plus `rotate_signing_key` (POST `/{id}/signing-key/rotate`), generated from the canonical `projects` OpenAPI spec and covered by success + error wire tests against the shared mock server. -## [3.0.2] - 2026-07-13 - -Parity release aligning the C++ SDK with the Python reference SDK across the -REST, RELAY, SWML, and SWAIG surfaces. The public API is generated from, and -continuously verified against, the shared SignalWire wire specification. - -### Added - Spec-generated REST surface: `RestClient` and its resource namespaces are generated from the canonical REST OpenAPI specs, replacing the hand-written resource classes. Every implemented route derives from the wire spec and is diff --git a/CLAUDE.md b/CLAUDE.md index 14c6b37..2f39d4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,8 @@ Prefer them over calling `clang-format` / `clang-tidy` / `run_tests` directly. bash scripts/run-format.sh # format the tree in place (clang-format -i) bash scripts/run-format.sh --check # verify-only (clang-format --dry-run -Werror); CI FMT gate bash scripts/run-lint.sh # lint (clang-tidy curated set, zero findings) +bash scripts/run-pylint.sh # lint + format the Python under scripts/ (ruff) +bash scripts/run-pylint.sh --check # verify-only; CI PY-LINT gate bash scripts/run-tests.sh # build + run the full test suite (run_tests) bash scripts/run-tests.sh rest_mock_ # run a subset (filter passed through to run_tests) ``` @@ -36,9 +38,33 @@ g++ -std=c++20 -I include -I deps examples/simple_agent.cpp -L build -lsignalwir ``` The full local-and-CI gate runner is `bash scripts/run-ci.sh`; its FMT / LINT / -TEST gates now delegate to the three scripts above (all four source +PY-LINT / TEST gates delegate to the scripts above (all source `scripts/_env.sh` for the clang-18 PATH bootstrap). +**Lint/format scope (widened 2026-07-30).** There is ONE bar, and it is the bar +the shipped library meets: + +| gate | covers | +|---|---| +| FMT | `src/` `include/` `tools/` `tests/` `examples/` `rest/examples/` `relay/examples/` | +| LINT | `src/` `include/` `tools/` `examples/` `rest/examples/` `relay/examples/` | +| PY-LINT | `scripts/*.py` | + +The only tree deliberately outside all of them is **`deps/`** — vendored +third-party code (httplib.h, json.hpp, nlohmann/) we do not own. That exclusion +is enforced at the compiler (CMake marks `deps/` a SYSTEM include directory), +not by a path list, because `clang-diagnostic-*` findings are compiler warnings +that clang-tidy's `--header-filter` cannot reach. `scripts/clang_tidy_cache.py` +is excluded from PY-LINT for the same reason: it is vendored verbatim from +matus-chochlik/ctcache at a pinned SHA. + +`tests/` is under FMT but **not yet under LINT** — a known, documented gap +awaiting an owner ruling, not a silent carve-out. Everything in `tests/` that is +not one of three specific checks has been burned to zero; the remainder is +structural (the `ASSERT_*` macro expansions, and the single-translation-unit +design in which `test_main.cpp` `#include`s 123 `.cpp` files). See the rationale +block at the top of `scripts/run-lint.sh`. + ## Architecture ### Directory Layout @@ -156,8 +182,10 @@ ctx.add_step("step1") - Library is built as a shared library `libsignalwire` (CMake `add_library(signalwire SHARED)`) - No package manager required; all deps vendored -- CPPHTTPLIB_OPENSSL_SUPPORT is disabled (requires OpenSSL 3.0+) -- SSL for httplib handled externally; crypto primitives use OpenSSL directly +- CPPHTTPLIB_OPENSSL_SUPPORT is **enabled** (`CMakeLists.txt:116`) — build with an + OpenSSL 3.0+ toolchain (1.1.1 is EOL). It gives `httplib::Client` `https://` for + REST and `httplib::SSLServer` for the webhook server (in-process TLS termination). +- Crypto primitives (HMAC-SHA256, random bytes) use OpenSSL directly - RELAY client: IXWebSocket-backed transport implemented (src/relay/websocket.cpp, client.cpp) — Blade/JSON-RPC session + real frame I/O - C wrapper (`signalwire_c.h`) provides FFI for other languages - Examples are standalone `.cpp` files meant to illustrate usage, not built by CMake diff --git a/CMakeLists.txt b/CMakeLists.txt index a640a21..a23efc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(signalwire VERSION 3.2.1 +project(signalwire VERSION 3.0.0 HOMEPAGE_URL "https://github.com/signalwire/signalwire-cpp" LANGUAGES CXX) # C++20: the SDK's documented, idiomatic construction of the generated *Params @@ -53,8 +53,15 @@ if(SIGNALWIRE_SANITIZE) message(STATUS "Sanitizer enabled: ${SIGNALWIRE_SANITIZE}") endif() -# Header-only deps -include_directories(${CMAKE_SOURCE_DIR}/deps) +# Header-only deps. SYSTEM, because deps/ is VENDORED THIRD-PARTY code we do not +# own (httplib.h, json.hpp, nlohmann/) -- marking it system tells the compiler to +# suppress warnings originating inside those headers, which is the correct +# mechanism for "not our code to fix". Without it, -Wall on the example targets +# surfaces httplib.h's own -Wmismatched-tags as findings against this repo, and +# clang-tidy's --header-filter cannot exclude them (clang-diagnostic-* are +# compiler warnings, not clang-tidy checks, so the filter does not apply). +# Our own include/ deliberately stays non-SYSTEM: we DO want its warnings. +include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps) include_directories(${CMAKE_SOURCE_DIR}/include) # OpenSSL @@ -92,6 +99,17 @@ file(GLOB_RECURSE SOURCES "src/*.cpp") # `g++ … -lsignalwire` against a static build fails to resolve OpenSSL/ixwebsocket # symbols (PACKAGE-SMOKE #92). The internal tools/tests below link it the same way. add_library(signalwire SHARED ${SOURCES}) + +# -Wall on the SHIPPED LIBRARY. Until 2026-07-30 -Wall was applied only to the +# example targets (the sole occurrence in this file), so the shipped code was +# held to a LOWER compiler-warning bar than its own demos -- backwards. It is +# PRIVATE so consumers of the installed target do not inherit it. +# +# Free to enable: the only two warnings left under -Wall are inside the vendored +# FetchContent IXWebSocket tree, not our code (first-party src/ + include/ is +# clean). The five -Woverloaded-virtual it used to report were the +# on_swml_request hiding, fixed by giving that hook its third parameter. +target_compile_options(signalwire PRIVATE -Wall) # cpp-httplib TLS enabled — requires OpenSSL 3.0+ (build with a 3.0+ toolchain; # OpenSSL 1.1.1 is EOL). Gives httplib::Client https:// for REST and # httplib::SSLServer for the webhook server (in-process TLS termination). @@ -133,9 +151,12 @@ endif() # place signalwire/ + nlohmann/json.hpp. target_include_directories(signalwire PUBLIC $ - $ $ $) +# deps/ is vendored third-party; SYSTEM so consumers do not inherit warnings +# from httplib.h / json.hpp as if they were ours (same reasoning as above). +target_include_directories(signalwire SYSTEM PUBLIC + $) # Namespaced alias so add_subdirectory/FetchContent consumers can use the SAME # `signalwire::signalwire` name the installed package exports. add_library(signalwire::signalwire ALIAS signalwire) @@ -385,6 +406,15 @@ target_include_directories(relay_liveness_dump PRIVATE ${CMAKE_SOURCE_DIR}/tests add_executable(secure_default_dump tools/secure_default_dump.cpp) target_link_libraries(secure_default_dump signalwire) +# token_interop_mint is the TOKEN-INTEROP fixture: it mints ONE SWAIG tool token +# with the FIXED inputs the checker supplies in the environment and prints just +# that token, so porting-sdk/scripts/diff_port_token_interop.py can validate it +# under the REFERENCE's own decoder. That is property 3 of the token contract — +# a correct key and a correct HMAC still leave the base64 ENVELOPE able to be +# wrong, and an unpadded token is unusable to every other implementation. +add_executable(token_interop_mint tools/token_interop_mint.cpp) +target_link_libraries(token_interop_mint signalwire) + # secret_scrub_dump is the SECRET-SCRUB-LIVE (PSDK-5, nightly) runner: it drives # the RELAY client through a real connect + an inbound authorization.state # re-auth frame at debug level with the fixture sentinels, captures fd 1 + fd 2 @@ -418,6 +448,15 @@ foreach(example_src ${EXAMPLE_SOURCES}) add_executable(${target_name} EXCLUDE_FROM_ALL ${example_src}) target_link_libraries(${target_name} signalwire) + # -Wno-unused-variable only: an example may legitimately declare a value to + # SHOW the API returns one without going on to use it. + # + # The -Wno-overloaded-virtual that briefly lived here is GONE, because the + # thing it was hiding is fixed: InfoGathererAgent::on_swml_request used to + # take 3 params and return json against a 2-param optional base, so it + # hid rather than overrode. The base now takes the reference's third + # `request` parameter and the prefab is a real override, so the warning has + # no site left to fire from. target_compile_options(${target_name} PRIVATE -Wall -Wno-unused-variable) add_dependencies(examples ${target_name}) endforeach() diff --git a/DOC_AUDIT_IGNORE.md b/DOC_AUDIT_IGNORE.md index 3a8fb8e..da710a8 100644 --- a/DOC_AUDIT_IGNORE.md +++ b/DOC_AUDIT_IGNORE.md @@ -26,7 +26,6 @@ at: nlohmann::json::at() / std::map::at() / std::vector::at() — vendored/stdli back: std::string::back() / std::vector::back() — stdlib container access begin: std::string::begin / std::vector::begin — stdlib iterator compare: std::string::compare() — stdlib string comparison -c_str: std::string::c_str() — stdlib C-string accessor (swmlservice_ai_sidecar.cpp httplib route registration) erase: std::string::erase / std::vector::erase — stdlib container mutate find_first_not_of: std::string::find_first_not_of() — stdlib string scan find_last_not_of: std::string::find_last_not_of() — stdlib string scan @@ -41,8 +40,9 @@ delete_: generated BaseResource/FabricResource DELETE verb — `delete` is a C++ ## 2. C++ standard library — algorithms, chrono, and C runtime array: std::array / nlohmann::json::array() — stdlib/vendored -atof: std::atof — C stdlib string-to-double atoi: std::atoi — C stdlib string-to-int +stod: std::stod — C++ stdlib string-to-double (replaced std::atof, which could not report a bad value) +stoi: std::stoi — C++ stdlib string-to-int (replaced std::atoi, which could not report a bad value) exit: std::exit — C stdlib process exit getline: std::getline — stdlib stream read hours: std::chrono::hours — stdlib duration literal diff --git a/PORT_ADDITIONS.md b/PORT_ADDITIONS.md index d006e3b..f2150bb 100644 --- a/PORT_ADDITIONS.md +++ b/PORT_ADDITIONS.md @@ -114,7 +114,6 @@ signalwire.core.contexts.Context.step_order: cpp_typed_accessor: const-reference signalwire.core.contexts.Context.steps: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. signalwire.core.contexts.Context.to_json: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. signalwire.core.contexts.Context.valid_contexts: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. -signalwire.core.contexts.ContextBuilder.attach_tool_name_supplier: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. signalwire.core.contexts.ContextBuilder.has_contexts: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. signalwire.core.contexts.ContextBuilder.to_json: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. signalwire.core.contexts.GatherInfo.completion_action: cpp_typed_accessor: const-reference accessor on the C++ class; Python exposes equivalent state via attribute reads not enumerated. @@ -213,7 +212,6 @@ signalwire.relay.call.Call.is_answered: cpp_typed_accessor: const-ref accessor o signalwire.relay.call.Call.is_ended: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. signalwire.relay.call.Call.node_id: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. signalwire.relay.call.Call.on_event: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. -signalwire.relay.call.Call.prompt: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. signalwire.relay.call.Call.record_call: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. signalwire.relay.call.Call.register_action: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. signalwire.relay.call.Call.resolve_all_actions: cpp_typed_accessor: const-ref accessor or state predicate on C++ relay::Call; Python exposes equivalent state via attribute reads not enumerated. @@ -434,7 +432,6 @@ signalwire.relay.message.Message.reason: cpp_accessor: failure reason populated signalwire.relay.call.Call.tap: cpp_naming: alias for tap_audio (the Python signalwire.relay.call.Call exposes both `tap` as the verb name and `tap_audio` as the method); C++ now provides tap as the canonical name with tap_audio kept as a backward-compatible alias. signalwire.relay.call.Call.transcribe: cpp_naming: alias for live_transcribe used by the mock-backed tests; the Python SDK has TranscribeAction returning from call.transcribe. signalwire.relay.call.Call.stream: cpp_unified_action: calling.stream verb — Python returns StreamAction; C++ exposes via the unified Action. -signalwire.relay.call.Call.play_and_collect: cpp_naming: alias for prompt(play_media, collect_params) — Python uses `play_and_collect` as the verb name on the wire and on the Call method (call.play_and_collect). C++ keeps prompt as the documented method; play_and_collect is the alias used by mock-backed tests. signalwire.relay.call.Call.pay: cpp_unified_action: calling.pay verb — Python returns PayAction; C++ via the unified Action. # C++-only additions on AgentBase / SWMLService surfaced by the signature audit. diff --git a/PORT_OMISSIONS.md b/PORT_OMISSIONS.md index 820e190..4396aa1 100644 --- a/PORT_OMISSIONS.md +++ b/PORT_OMISSIONS.md @@ -96,7 +96,7 @@ signalwire.core.agent.tools.registry.ToolRegistry.register_class_decorated_tools signalwire.core.contexts.create_simple_context: impossible: Python module-level convenience factory returning a ContextBuilder from **kwargs; C++ uses contexts::ContextBuilder directly — the free-function FORM has no static-C++ analog (Java/TS/PHP construct the builder directly likewise) signalwire.core.data_map.create_expression_tool: impossible: Python module-level factory composing an expression tool from **kwargs + a callable pattern-map; C++'s datamap::DataMap builds the same wire shape fluently — the free-function FORM has no static-C++ analog (Java/TS/PHP compose fluently likewise) signalwire.core.data_map.create_simple_api_tool: impossible: Python module-level factory composing an API tool from **kwargs; C++ builds the same wire shape fluently via datamap::DataMap(...).webhook(...).output(...) — the free-function FORM has no static-C++ analog (Java/TS/PHP compose fluently likewise) -signalwire.core.function_result.FunctionResult.to_dict: python_collapsed: Python to_dict() returns a Python dict; C++ to_json() returns the equivalent nlohmann::json. to_dict name is intentionally replaced by to_json. +signalwire.core.function_result.FunctionResult.to_dict: cpp_serializer_to_json: the C++ serializer is `json to_json() const` (include/signalwire/swaig/function_result.hpp:421) returning nlohmann::json where the reference records to_dict() -> dict. The SURFACE enumerator aliases to_json->to_dict, so this entry is DEAD for SURFACE-DIFF; it is live only for the signature gate, which has no such alias and reports to_dict as missing-port. Same serialization (byte-compared against Python to_dict() by the EMISSION gate over the shared 81-entry corpus) — serializer-name idiom. agentbase-family.tool: impossible: Python @tool decorator method relies on the decorator protocol; C++ has no method-decorator feature — tools register via define_tool(...) directly (Java/TS/PHP omit as impossible). Re-keyed to agentbase-family by the A-fold (ALLOWLIST_DISCIPLINE §4c). agentbase-family.get_app: impossible: returns a FastAPI/Flask ASGI/WSGI app object; C++ has no such framework — the service runs httplib directly, so there is no app object to return (Java/TS/PHP omit the framework-app FORM identically). Re-keyed to agentbase-family by the A-fold (ALLOWLIST_DISCIPLINE §4c). signalwire.core.swml_builder.SWMLBuilder.__getattr__: impossible: Python runtime __getattr__ dynamic verb dispatch; C++ has no __getattr__/method_missing analog — SWMLBuilder expands each named verb (answer/hangup/ai/play/say) as an explicit method (Java/TS/PHP expand identically) @@ -113,7 +113,6 @@ signalwire.skills.mcp_gateway.skill.MCPGatewaySkill: approved: MCP gateway subsy signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.get_global_data: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.get_hints: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.get_parameter_schema: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. -signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.get_prompt_sections: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.register_tools: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.setup: approved: MCP gateway subsystem is Python-only, not ported to any SDK (user ruling; §I.1). C++ agents consume MCP via agent.add_mcp_server/enable_mcp_server on AgentBase, which are implemented; the standalone MCPGatewaySkill is not ported. @@ -131,12 +130,10 @@ signalwire.core.swml_service.SWMLService.verb_registry: impossible: Python SWMLS # Auto-extras: missing-from-omissions entries surfaced when the surface-audit # diff step started running after the wire-up bug was fixed. signalwire.core.security.webhook_middleware.make_webhook_validation_dependency: impossible: framework-bound factory returning a FastAPI dependency callable; the framework WRAPPER has no C++ analog (C++ has no FastAPI). The framework-free decision CORE is reconciled — C++ ships webhook_middleware.validate as the free function signalwire::security::Validate(method,url,headers,body,signing_key)->optional<(status,headers,body)> (matched 1:1 by the DRIFT gate via the free-function rename table), and the cpp-httplib WrapWithSignatureValidation adapter (a PORT_ADDITION) is the native middleware idiom on top of it. Only the FastAPI-dependency FORM stays idiom (Java/TS/PHP ship native middleware likewise). -signalwire.skills.api_ninjas_trivia.skill.ApiNinjasTriviaSkill.get_tools: python_mixin_collapsed: C++ skills are implemented as C++ classes registered with skills::SkillRegistry; per-skill Python method names (setup/register_tools/get_hints/get_parameter_schema) are implementation internals and not exposed by name in C++. User-visible behavior reachable via agent.add_skill("", params). -signalwire.skills.play_background_file.skill.PlayBackgroundFileSkill.get_tools: python_mixin_collapsed: C++ skills are implemented as C++ classes registered with skills::SkillRegistry; per-skill Python method names (setup/register_tools/get_hints/get_parameter_schema) are implementation internals and not exposed by name in C++. User-visible behavior reachable via agent.add_skill("", params). -signalwire.skills.spider.skill.SpiderSkill.__init__: python_mixin_collapsed: C++ skills are implemented as C++ classes registered with skills::SkillRegistry; per-skill Python method names (setup/register_tools/get_hints/get_parameter_schema) are implementation internals and not exposed by name in C++. User-visible behavior reachable via agent.add_skill("", params). -signalwire.skills.weather_api.skill.WeatherApiSkill.get_tools: python_mixin_collapsed: C++ skills are implemented as C++ classes registered with skills::SkillRegistry; per-skill Python method names (setup/register_tools/get_hints/get_parameter_schema) are implementation internals and not exposed by name in C++. User-visible behavior reachable via agent.add_skill("", params). -signalwire.skills.wikipedia_search.skill.WikipediaSearchSkill.search_wiki: python_mixin_collapsed: C++ skills are implemented as C++ classes registered with skills::SkillRegistry; per-skill Python method names (setup/register_tools/get_hints/get_parameter_schema) are implementation internals and not exposed by name in C++. User-visible behavior reachable via agent.add_skill("", params). -signalwire.web.web_service.WebService: python_collapsed: Python WebService hosts async HTTP endpoints for multi-agent serving; C++ uses AgentServer + httplib for the same purpose. A separate WebService class is intentionally folded into AgentServer. +signalwire.skills.api_ninjas_trivia.skill.ApiNinjasTriviaSkill.get_tools: cpp_builtin_skill_in_tu: the C++ built-in skills are DEFINED IN THE .cpp TRANSLATION UNIT (src/skills/builtin/api_ninjas_trivia.cpp) and self-register with skills::SkillRegistry — there is no public header declaring the class, so the libclang SIGNATURE enumerator (which walks include/ only) records no member for it. The regex SURFACE enumerator DOES see the class, so this entry is dead for SURFACE-DIFF and live only for the signature gate. Not a missing capability: reachable via agent.add_skill("api_ninjas_trivia", params). Fix is enumerator scope (walk src/skills/builtin), not the port. +signalwire.skills.play_background_file.skill.PlayBackgroundFileSkill.get_tools: cpp_builtin_skill_in_tu: the C++ built-in skills are DEFINED IN THE .cpp TRANSLATION UNIT (src/skills/builtin/play_background_file.cpp) and self-register with skills::SkillRegistry — there is no public header declaring the class, so the libclang SIGNATURE enumerator (which walks include/ only) records no member for it. The regex SURFACE enumerator DOES see the class, so this entry is dead for SURFACE-DIFF and live only for the signature gate. Not a missing capability: reachable via agent.add_skill("play_background_file", params). Fix is enumerator scope (walk src/skills/builtin), not the port. +signalwire.skills.weather_api.skill.WeatherApiSkill.get_tools: cpp_builtin_skill_in_tu: the C++ built-in skills are DEFINED IN THE .cpp TRANSLATION UNIT (src/skills/builtin/weather_api.cpp) and self-register with skills::SkillRegistry — there is no public header declaring the class, so the libclang SIGNATURE enumerator (which walks include/ only) records no member for it. The regex SURFACE enumerator DOES see the class, so this entry is dead for SURFACE-DIFF and live only for the signature gate. Not a missing capability: reachable via agent.add_skill("weather_api", params). Fix is enumerator scope (walk src/skills/builtin), not the port. +signalwire.skills.wikipedia_search.skill.WikipediaSearchSkill.search_wiki: cpp_builtin_skill_in_tu: search_wiki IS implemented in C++ (src/skills/builtin/wikipedia_search.cpp:38, `std::string search_wiki(const std::string& query) const`) but the class is defined in the .cpp TRANSLATION UNIT with no public header, so the libclang SIGNATURE enumerator (which walks include/ only) records no member for it. The regex SURFACE enumerator DOES see the class, so this entry is dead for SURFACE-DIFF and live only for the signature gate. Fix is enumerator scope (walk src/skills/builtin), not the port. signalwire.rest._request_options.resolve: impossible: Python module-level free function resolving effective options (per-request over client-default over built-in); C++ folds this into HttpClient's private request() funnel (an anonymous-namespace resolve() in http_client.cpp) — the module-free-function FORM has no static-C++ analog (Java/TS/PHP fold it into the client likewise) signalwire.rest._request_options.status_is_retryable: impossible: Python module-level free function deciding idempotency-aware retryability; C++ folds this into HttpClient's private request() funnel (an anonymous-namespace status_is_retryable() in http_client.cpp) — the module-free-function FORM has no static-C++ analog (Java/TS/PHP fold it into the client likewise) diff --git a/PORT_SIGNATURE_OMISSIONS.md b/PORT_SIGNATURE_OMISSIONS.md index f0a83d8..3c336d8 100644 --- a/PORT_SIGNATURE_OMISSIONS.md +++ b/PORT_SIGNATURE_OMISSIONS.md @@ -72,14 +72,6 @@ diverge from Python's. return is structurally a subtype of the C++ unified Action — same callable contract, lower static guarantee. Tracked for audit clarity; not load-bearing for cross-language code. -- `cpp_constructor_default_only`: C++ ships an explicit default-only or - config-struct constructor where Python's `__init__` enumerates each - field as a keyword argument. Construction is reached either by - calling the no-arg constructor and using setters, or by passing a - pre-built config struct (`RelayClient::Config`, - `AgentBase::Builder`). The full set of Python `__init__` keywords is - reachable through the C++ setters / config-struct fields — same - callable contract, different idiomatic shape. - `cpp_typed_overload_subset`: C++ exposes a smaller-arity overload of the method where Python merges all variants into one signature with default-valued kwargs. The remaining options are reached either via @@ -102,9 +94,6 @@ diverge from Python's. passes the variant as a single parameter (e.g. `Call::join_conference(name)` vs `Call.join_conference(**kwargs)`). Same callable contract; C++ just static-types the call site. -- `cpp_idiom_optional_int_timeout`: C++ uses `int` (with `0` meaning - "no timeout") where Python uses `Optional[float]`. Same semantics; - C++ avoids `std::optional` for the canonical no-timeout sentinel. - `cpp_action_collapsed_return`: paired with `cpp_unified_action`. C++ methods on Call return the unified `Action` while Python returns per-verb subclasses (`PlayAction`, `RecordAction`, ...). The Action @@ -158,27 +147,11 @@ diverge from Python's. - `cpp_pattern_string`: Python's `DataMap.expression(pattern)` accepts `Union[Pattern, str]`; C++ accepts `string` only and compiles internally. Same regex contract. -- `cpp_postal_code_string`: Python's `FunctionResult.pay(postal_code: - Union[bool, str])` accepts a sentinel boolean to mean "ask for - postal code at runtime"; C++ accepts only `string` (the empty - string acts as the sentinel). Same wire payload. -- `cpp_debug_level_bool`: Python's - `AIConfigMixin.enable_debug_events(level: int)` accepts a verbosity - level; C++ accepts a bool (on/off). The level-of-detail modes are - not yet ported; the boolean variant covers the common - enable-or-disable case. - `cpp_load_skill_signature`: Python's `SkillManager.load_skill(skill_name, ..., params)` accepts a `params: dict` runtime configuration; C++ accepts the parent agent reference instead, with skill-specific options reached via `SkillBase` setter methods. Same load-time configuration contract. -- `cpp_questions_string`: Python's - `InfoGathererAgent.__init__(questions: list[dict])` accepts a list - of typed question dicts; C++ accepts a `string` JSON spec for the - same data. Construction-time only. -- `cpp_dial_int_timeout`: paired with `cpp_idiom_optional_int_timeout` - — `RelayClient.dial(dial_timeout: int)` uses `0` for "no - timeout"; Python uses `Optional[float]`. - `cpp_register_skill_factory`: Python's `SkillRegistry.register_skill(skill_class)` accepts a class object (Python's metaclass machinery introspects it for name/factory); @@ -224,10 +197,6 @@ diverge from Python's. optional `params` query-string dict; C++ omits this — POST URLs with query params aren't used by the SignalWire REST API surface C++ targets. -- `cpp_rest_error_field_layout`: C++ `SignalWireRestError.__init__` - takes `(status, message, body)`; Python takes - `(status_code, body, url, method)`. The two carry the same - diagnostic content under different field names. - `cpp_typed_setter_no_extra_dict`: Python's `PhoneNumbersResource.set_*` helpers accept an `extra: dict` catch-all for fields the typed setters don't enumerate; C++ @@ -248,27 +217,6 @@ diverge from Python's. ## Documented signature divergences -### __init__ default-only / config-struct construction - -signalwire.agent_server.AgentServer.__init__: cpp_constructor_default_only -signalwire.core.agent_base.AgentBase.__init__: cpp_constructor_default_only -signalwire.core.contexts.Context.__init__: cpp_constructor_default_only -signalwire.core.contexts.ContextBuilder.__init__: cpp_constructor_default_only -signalwire.core.contexts.Step.__init__: cpp_constructor_default_only -signalwire.core.security.session_manager.SessionManager.__init__: cpp_constructor_default_only -signalwire.core.skill_base.SkillBase.__init__: cpp_constructor_default_only -signalwire.core.skill_manager.SkillManager.__init__: cpp_constructor_default_only -signalwire.core.swml_service.SWMLService.__init__: cpp_constructor_default_only -signalwire.prefabs.concierge.ConciergeAgent.__init__: cpp_constructor_default_only -signalwire.prefabs.faq_bot.FAQBotAgent.__init__: cpp_constructor_default_only -signalwire.prefabs.receptionist.ReceptionistAgent.__init__: cpp_constructor_default_only -signalwire.prefabs.survey.SurveyAgent.__init__: cpp_constructor_default_only -signalwire.relay.call.Call.__init__: cpp_constructor_default_only -signalwire.relay.client.RelayClient.__init__: cpp_constructor_default_only -signalwire.relay.message.Message.__init__: cpp_constructor_default_only -signalwire.rest._base.SignalWireRestError.__init__: cpp_rest_error_field_layout -signalwire.prefabs.info_gatherer.InfoGathererAgent.__init__: cpp_questions_string - ### Unified Action — Call methods signalwire.relay.call.Call.ai: cpp_unified_action @@ -315,7 +263,7 @@ signalwire.relay.message.Message.wait: cpp_wait_returns_bool signalwire.relay.client.RelayClient.connect: cpp_connect_returns_bool signalwire.relay.client.RelayClient.on_call: cpp_handler_register_void signalwire.relay.client.RelayClient.on_message: cpp_handler_register_void -signalwire.relay.client.RelayClient.dial: cpp_dial_int_timeout +signalwire.relay.client.RelayClient.dial: cpp_positional_kwargs: the reference declares `dial(devices, *, tag, max_duration, dial_timeout)` — the last three are KEYWORD-ONLY — while C++ has no keyword-only parameter form, so all three land as positional (client.hpp:136). The `devices` param is nlohmann::json (projected to `any`) where the reference types it list>>. NOTE: the timeout TYPE no longer diverges — C++ declares `std::optional dial_timeout = std::nullopt` in SECONDS, matching the reference's `Optional[float]` (the old `dial_timeout_ms: int` / `0 == no timeout` shape was fixed; see the past-tense note at client.hpp:120-135). ### Typed-overload subset (C++ ships fewer kwargs) @@ -332,7 +280,6 @@ signalwire.core.agent_base.AgentBase.on_summary: cpp_typed_overload_subset signalwire.core.mixins.ai_config_mixin.AIConfigMixin.add_function_include: cpp_typed_overload_subset signalwire.core.mixins.ai_config_mixin.AIConfigMixin.add_internal_filler: cpp_typed_overload_subset signalwire.core.mixins.ai_config_mixin.AIConfigMixin.add_language: cpp_typed_overload_subset -signalwire.core.mixins.ai_config_mixin.AIConfigMixin.add_pattern_hint: cpp_typed_overload_subset signalwire.core.mixins.auth_mixin.AuthMixin.get_basic_auth_credentials: cpp_overload_for_optional_kw signalwire.core.mixins.prompt_mixin.PromptMixin.define_contexts: cpp_define_contexts_typed_return signalwire.core.mixins.prompt_mixin.PromptMixin.prompt_add_section: cpp_typed_overload_subset @@ -358,8 +305,6 @@ signalwire.core.agent.tools.registry.ToolRegistry.get_function: cpp_typed_tool_p signalwire.core.agent_base.AgentBase.on_debug_event: cpp_callable_typedef signalwire.core.contexts.Context.add_step: cpp_typed_step_positional signalwire.core.data_map.DataMap.expression: cpp_pattern_string -signalwire.core.function_result.FunctionResult.pay: cpp_postal_code_string -signalwire.core.mixins.ai_config_mixin.AIConfigMixin.enable_debug_events: cpp_debug_level_bool signalwire.core.mixins.ai_config_mixin.AIConfigMixin.set_languages: cpp_typed_overload_subset signalwire.core.mixins.ai_config_mixin.AIConfigMixin.set_pronunciations: cpp_typed_overload_subset signalwire.core.mixins.prompt_mixin.PromptMixin.get_prompt: cpp_get_prompt_string_only @@ -371,12 +316,10 @@ signalwire.skills.registry.SkillRegistry.list_skills: cpp_list_skills_names ## POM (signalwire.pom.pom) — C++ idiom -signalwire.pom.pom.PromptObjectModel.__init__: cpp-overload-set — C++ exposes overloaded ctors (default, copy-from-list, copy-from-PromptObjectModel) where Python has a single __init__ with default arg signalwire.pom.pom.PromptObjectModel.add_section: cpp-overload-set — C++ exposes 4 overloads (title-only / title+body / title+bullets / full) where Python uses single positional+kwargs signalwire.pom.pom.PromptObjectModel.add_pom_as_subsection: cpp-typed-overload — C++ takes typed Section& or std::string title parameter where Python uses Union[str, Section] signalwire.pom.pom.PromptObjectModel.from_json: cpp-typed-overload — C++ takes const std::string& where Python's from_json takes Union[str, dict] signalwire.pom.pom.PromptObjectModel.from_yaml: cpp-typed-overload — C++ takes const std::string& where Python's from_yaml takes Union[str, dict] -signalwire.pom.pom.Section.__init__: cpp-overload-set — C++ exposes overloaded ctors (default, builder, copy) where Python has a single __init__ with positional+kwargs signalwire.pom.pom.Section.add_subsection: cpp-overload-set — C++ exposes 4 overloads (title-only / title+body / title+bullets / full) where Python uses single positional+kwargs ## Webhook signature validation (signalwire.core.security.*) — C++ idiom @@ -384,11 +327,8 @@ signalwire.pom.pom.Section.add_subsection: cpp-overload-set — C++ exposes 4 ov signalwire.core.security.webhook_validator.validate_request: cpp-typed-overload — C++ ParamsOrBody is std::variant>>> covering raw-body and pre-parsed form-params; Python's Union additionally lists Mapping[str,Any] and None which collapse to the same Scheme B path. Same wire contract — different idiomatic typing. # ---- item H/I signature idioms (relay events/actions, bedrock, core infra, mixins, prefabs) ---- -signalwire.agent_server.AgentServer.agents: cpp_property_via_getter: Python exposes `agents` as a @property returning dict; the C++ port exposes the same registry via the named getter get_agents() (returning vector>) — the getter is already the parity method (surface-matched); the bare property name has no distinct C++ symbol. Same registered-agents access, property-vs-getter idiom (not a kwargs spread). signalwire.agent_server.AgentServer.register_global_routing_callback: cpp_typed_callback: the port takes a concrete GlobalRoutingCallback functor class where the Python reference records a bare callable<[dict,dict],optional> annotation, and returns AgentServer& (fluent) where Python returns void; same multi-agent routing registration — the typed callback + fluent return are the C++ idiom, not a kwargs spread. signalwire.core.agent_base.AgentBase.add_swaig_query_params: cpp_json_param_untyped: the C++ param is nlohmann::json (projected to `any`) where the Python reference types it concretely (dict); same query-param map, the open json param is the C++ carrier — NOT a **kwargs spread (the oracle records a single typed param, no var_keyword). -signalwire.core.auth_handler.AuthHandler.verify_basic_auth: cpp_idiom_carrier: AuthHandler verify_* take typed C++ credential carriers (BasicCredentials/BearerCredentials) where Python takes framework HTTPBasicCredentials/HTTPAuthorizationCredentials; same auth check (mirrors Java's records). -signalwire.core.auth_handler.AuthHandler.verify_bearer_token: cpp_idiom_carrier: AuthHandler verify_* take typed C++ credential carriers (BasicCredentials/BearerCredentials) where Python takes framework HTTPBasicCredentials/HTTPAuthorizationCredentials; same auth check (mirrors Java's records). signalwire.core.contexts.Context.set_enter_fillers: cpp_json_param_untyped: the C++ param is nlohmann::json (projected to `any`) where the Python reference types it concretely (dict>); same enter-fillers map, open-json carrier idiom (not a kwargs spread). signalwire.core.contexts.Context.set_exit_fillers: cpp_json_param_untyped: the C++ param is nlohmann::json (projected to `any`) where the Python reference types it concretely (dict>); same exit-fillers map, open-json carrier idiom (not a kwargs spread). signalwire.core.contexts.Context.to_dict: cpp_serializer_to_json: the C++ Context serializer is to_json() returning nlohmann::json (projected to `any`); the Python reference records the same serializer as to_dict() -> dict. The surface enumerator already aliases to_json->to_dict for membership; the signature-level residual is only the json/`any` vs concrete-dict return (json IS the open `any` type). Same on-wire serialization, serializer idiom (not a kwargs spread). @@ -409,8 +349,6 @@ signalwire.core.mixins.web_mixin.WebMixin.register_routing_callback: cpp_typed_c signalwire.core.pom_builder.PomBuilder.from_sections: cpp_json_param_untyped: the C++ sections param is nlohmann::json (projected to `any`) where the Python reference types it list>; same section list, open-json carrier idiom (mirrors Java PomBuilder; not a kwargs spread). signalwire.core.security.security_utils.filter_sensitive_headers: cpp_concrete_map: the C++ filter_sensitive_headers takes/returns map where the Python reference records a generic TypeVar _V (dict); the C++ header map is the concrete instantiation — same header-hygiene behavior, concrete-type idiom (not a kwargs spread). signalwire.core.security_config.SecurityConfig.validate_ssl_config: cpp_kwargs_positional: SecurityConfig ctor takes typed C++ params where Python takes config_file/service_name keyword args; same env-driven config (mirrors Java SecurityConfig). -signalwire.core.skill_manager.SkillManager.loaded_skills: cpp_property_via_getter: Python exposes `loaded_skills` as a @property returning dict; the C++ port exposes the same via the named getter list_loaded_skills() — the getter is already the parity method (surface-matched); the bare property name has no distinct C++ symbol. Same loaded-skills access, property-vs-getter idiom (not a kwargs spread). -signalwire.core.swaig_function.SWAIGFunction.__init__: cpp_typed_callback_plus_json: the C++ ctor takes a concrete SwaigFunctionHandler class where the Python reference records a bare callable<[any],any>, and its parameters argument is nlohmann::json (projected to `any`) where Python types it optional>; plus a trailing json extra_swaig_fields carrier — same SWAIG descriptor, typed-handler + open-json idiom (not a kwargs spread). signalwire.core.swaig_function.SWAIGFunction.validate_args: cpp_typed_return: the C++ validate_args returns a concrete ArgsValidationResult where the Python reference returns a raw tuple; the args param is nlohmann::json (projected to `any`) where Python types it dict — same validation contract, typed-return-object idiom (not a kwargs spread). signalwire.core.swml_builder.SWMLBuilder.add_section: cpp_fluent_self: SWMLBuilder verb methods return the concrete SWMLBuilder& (fluent chaining) where Python's type hint is Self; and kwargs land as a trailing nlohmann::json — same document, C++ builder idiom. signalwire.core.swml_builder.SWMLBuilder.ai: cpp_fluent_self: SWMLBuilder verb methods return the concrete SWMLBuilder& (fluent chaining) where Python's type hint is Self; and kwargs land as a trailing nlohmann::json — same document, C++ builder idiom. @@ -419,9 +357,7 @@ signalwire.core.swml_builder.SWMLBuilder.hangup: cpp_fluent_self: SWMLBuilder ve signalwire.core.swml_builder.SWMLBuilder.play: cpp_fluent_self: SWMLBuilder verb methods return the concrete SWMLBuilder& (fluent chaining) where Python's type hint is Self; and kwargs land as a trailing nlohmann::json — same document, C++ builder idiom. signalwire.core.swml_builder.SWMLBuilder.reset: cpp_fluent_self: SWMLBuilder verb methods return the concrete SWMLBuilder& (fluent chaining) where Python's type hint is Self; and kwargs land as a trailing nlohmann::json — same document, C++ builder idiom. signalwire.core.swml_builder.SWMLBuilder.say: cpp_fluent_self: SWMLBuilder verb methods return the concrete SWMLBuilder& (fluent chaining) where Python's type hint is Self; and kwargs land as a trailing nlohmann::json — same document, C++ builder idiom. -signalwire.core.swml_handler.AIVerbHandler.build_config: cpp_options_object: the C++ AIVerbHandler.build_config takes a single nlohmann::json config object collapsing Python's typed keyword params (prompt_text, prompt_pom, contexts, post_prompt, post_prompt_url, swaig), and returns json (projected to `any`) where Python returns a concrete dict; same AI-verb config shape — options-object idiom (not a **kwargs spread; the oracle records named typed params, no var_keyword). signalwire.core.swml_handler.SWMLVerbHandler.validate_config: cpp_typed_return: the C++ SWMLVerbHandler.validate_config returns a concrete VerbValidationResult where the Python reference returns a raw tuple>; the config param is json (projected to `any`) where Python types it dict — same verb-validation contract, typed-return-object idiom (not a kwargs spread). -signalwire.core.swml_renderer.SwmlRenderer.render_swml: cpp_options_struct: SwmlRenderer.render_swml collapses Python's ~14 keyword render options into a RenderOptions struct arg (the port's options-object idiom, mirrors Java's RenderOptions) — same rendered SWML. signalwire.core.swml_service.SWMLService.add_verb_to_section: cpp_fluent_return: the C++ SWMLService.add_verb_to_section returns SWMLService& (fluent chaining) where the Python reference returns bool; the config param is json (projected to `any`) where Python types it union — same add-verb behavior, fluent-return + open-json idiom (not a kwargs spread). signalwire.core.swml_service.SWMLService.register_routing_callback: cpp_typed_callback: SWMLService takes a concrete RoutingCallback functor class where the Python reference records a bare callable<[dict,dict],optional> annotation; same routing-callback registration — the typed callback is the C++ idiom, not a kwargs spread. signalwire.prefabs.concierge.ConciergeAgent.on_summary: cpp_overload: the C++ prefab method is a real handler/callback whose signature the reference records once (on_summary/on_swml_request take the C++ request+headers form); same behavior, port overload idiom (mirrors Java prefabs). @@ -431,40 +367,27 @@ signalwire.prefabs.info_gatherer.InfoGathererAgent.set_question_callback: cpp_ov signalwire.prefabs.receptionist.ReceptionistAgent.on_summary: cpp_overload: the C++ prefab method is a real handler/callback whose signature the reference records once (on_summary/on_swml_request take the C++ request+headers form); same behavior, port overload idiom (mirrors Java prefabs). signalwire.prefabs.survey.SurveyAgent.on_summary: cpp_overload: the C++ prefab method is a real handler/callback whose signature the reference records once (on_summary/on_swml_request take the C++ request+headers form); same behavior, port overload idiom (mirrors Java prefabs). signalwire.register_skill: cpp_typed_callback: the top-level register_skill free function takes a factory callable<[],SkillBase> where the Python reference takes the SkillBase *type object* directly; C++ has no first-class type value, so registration is by factory — same skill registration, factory-callable idiom (not a kwargs spread). -signalwire.relay.call.AIAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.Action.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.Action.result: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). signalwire.relay.call.Action.wait: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.Call.ai_hold: cpp_options_object: the C++ Call.ai_hold takes a single nlohmann::json params object collapsing Python's typed keyword params (timeout, prompt), and returns relay::Action where Python returns the raw dict; same calling.ai_hold wire frame (verified vs relay_apis.c) — options-object + typed-return idiom, NOT a **kwargs spread (the oracle records named typed params, no var_keyword). -signalwire.relay.call.Call.ai_message: cpp_options_object: the C++ Call.ai_message takes a single nlohmann::json params object collapsing Python's typed keyword params (message_text, role, reset, global_data), and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). +signalwire.relay.call.Call.ai_hold: cpp_typed_return: the C++ Call.ai_hold returns relay::Action where the Python reference returns the raw dict; params match (timeout, prompt) — same calling.ai_hold wire frame (verified vs relay_apis.c:2006), typed-return-object idiom. +signalwire.relay.call.Call.ai_message: cpp_typed_return: the C++ Call.ai_message returns relay::Action where the Python reference returns the raw dict; params match (message_text, role, reset, global_data) — same wire frame (verified vs relay_apis.c), typed-return-object idiom. signalwire.relay.call.Call.ai_unhold: cpp_typed_return: the C++ Call.ai_unhold takes prompt positionally (Python marks it keyword) as a json-carried value and returns relay::Action where Python returns the raw dict; same calling.ai_unhold wire frame (verified vs relay_apis.c) — positional + typed-return idiom (not a kwargs spread). -signalwire.relay.call.Call.amazon_bedrock: cpp_options_object: the C++ Call.amazon_bedrock takes a single nlohmann::json params object collapsing Python's typed keyword params (prompt, SWAIG, ai_params, global_data, post_prompt, post_prompt_url), and returns relay::Action where Python returns the raw dict; same calling.amazon_bedrock wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). -signalwire.relay.call.Call.bind_digit: cpp_options_object: the C++ Call.bind_digit takes typed (digits, bind_method) plus a single nlohmann::json params object collapsing Python's remaining typed keyword params (bind_params, realm, max_triggers), and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). +signalwire.relay.call.Call.amazon_bedrock: cpp_typed_return: the C++ Call.amazon_bedrock returns relay::Action where the Python reference returns the raw dict; params match (prompt, SWAIG, ai_params, global_data, post_prompt, post_prompt_url) — same calling.amazon_bedrock wire frame (verified vs relay_apis.c:1982), typed-return-object idiom. +signalwire.relay.call.Call.bind_digit: cpp_typed_return: the C++ Call.bind_digit returns relay::Action where the Python reference returns the raw dict; params match (digits, bind_method, bind_params, realm, max_triggers) — same wire frame (verified vs relay_apis.c:1479), typed-return-object idiom. signalwire.relay.call.Call.clear_digit_bindings: cpp_typed_return: the C++ Call.clear_digit_bindings takes realm positionally (Python marks it keyword) and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — positional + typed-return idiom (not a kwargs spread). signalwire.relay.call.Call.denoise: cpp_typed_return: the C++ Call.denoise returns relay::Action where the Python reference returns the raw dict; params match — same calling.denoise wire frame (verified vs relay_apis.c), typed-return-object idiom (not a kwargs spread). signalwire.relay.call.Call.denoise_stop: cpp_typed_return: the C++ Call.denoise_stop returns relay::Action where the Python reference returns the raw dict; params match — same wire frame (verified vs relay_apis.c), typed-return-object idiom (not a kwargs spread). -signalwire.relay.call.Call.echo: cpp_options_object: the C++ Call.echo takes a single nlohmann::json params object collapsing Python's typed keyword params (timeout, status_url), and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). +signalwire.relay.call.Call.echo: cpp_typed_return: the C++ Call.echo returns relay::Action where the Python reference returns the raw dict; params match (timeout, status_url) — same calling.echo wire frame (verified vs relay_apis.c), typed-return-object idiom. signalwire.relay.call.Call.leave_conference: cpp_typed_return: the C++ Call.leave_conference returns relay::Action where the Python reference returns the raw dict; the conference_id param is typed and matches — same wire frame (verified vs relay_apis.c), typed-return-object idiom (not a kwargs spread). signalwire.relay.call.Call.leave_room: cpp_typed_return: the C++ Call.leave_room returns relay::Action where the Python reference returns the raw dict; params match — same wire frame (verified vs relay_apis.c), typed-return-object idiom (not a kwargs spread). signalwire.relay.call.Call.on: cpp_typed_callback: the C++ Call.on binds a single typed CallEvent handler (callable<[CallEvent],void>) where the Python reference takes (event_type, handler) with an EventHandler class; the C++ event kind is carried by the typed handler / dispatch rather than a leading string param — same event subscription, typed-callback idiom. signalwire.relay.call.Call.pass_: cpp_typed_return: the C++ Call.pass_ returns relay::Action where the Python reference returns the raw dict; params match — same wire frame (verified vs relay_apis.c), typed-return-object idiom (not a kwargs spread). -signalwire.relay.call.Call.queue_enter: cpp_options_object: the C++ Call.queue_enter takes typed (queue_name) plus a single nlohmann::json params object collapsing Python's remaining typed keyword params (control_id, status_url), and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). -signalwire.relay.call.Call.queue_leave: cpp_options_object: the C++ Call.queue_leave takes typed (queue_name) plus a single nlohmann::json params object collapsing Python's remaining typed keyword params (control_id, queue_id, status_url), and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — options-object + typed-return idiom (not a kwargs spread). +signalwire.relay.call.Call.queue_enter: cpp_typed_return: the C++ Call.queue_enter returns relay::Action where the Python reference returns the raw dict; params match (queue_name, control_id, status_url) — same calling.queue.enter wire frame (verified vs relay_apis.c:1333), typed-return-object idiom. +signalwire.relay.call.Call.queue_leave: cpp_typed_return: the C++ Call.queue_leave returns relay::Action where the Python reference returns the raw dict; params match (queue_name, control_id, queue_id, status_url) — same calling.queue.leave wire frame (verified vs relay_apis.c), typed-return-object idiom. signalwire.relay.call.Call.refer: cpp_typed_return: the C++ Call.refer takes device (json) + status_url positionally (Python marks status_url keyword) and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — positional + open-json + typed-return idiom (not a kwargs spread). signalwire.relay.call.Call.user_event: cpp_typed_return: the C++ Call.user_event takes event positionally (Python marks it keyword) and returns relay::Action where Python returns the raw dict; same wire frame (verified vs relay_apis.c) — positional + typed-return idiom (not a kwargs spread). signalwire.relay.call.Call.wait_for: cpp_verb_shape: the C++ Call.wait_for is a call-state waiter — (target_state, timeout_ms) -> bool — where the Python reference's wait_for is a RELAY-event waiter (event_type, predicate, timeout) -> RelayEvent; the two expose different wait surfaces under the same name (the C++ event-wait path is Call.on / the typed event handlers). Kept as a documented signature divergence — NOT a kwargs spread. -signalwire.relay.call.CollectAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). signalwire.relay.call.CollectAction.start_input_timers: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.DetectAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.FaxAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.PayAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.PlayAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.RecordAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.StandaloneCollectAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). signalwire.relay.call.StandaloneCollectAction.start_input_timers: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.StreamAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.TapAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). -signalwire.relay.call.TranscribeAction.__init__: cpp_unified_action: C++ flattens every RELAY call-action onto a single relay::Action (concrete PlayAction/RecordAction/... inherit its ctor); the projected __init__/start_input_timers carry the unified Action's signature, not Python's per-subclass one (documented cpp_unified_action idiom). signalwire.relay.call.AIAction.stop: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. signalwire.relay.call.CollectAction.pause: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. signalwire.relay.call.CollectAction.resume: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. @@ -484,59 +407,10 @@ signalwire.relay.call.StandaloneCollectAction.stop: cpp_unified_action: the conc signalwire.relay.call.StreamAction.stop: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. signalwire.relay.call.TapAction.stop: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. signalwire.relay.call.TranscribeAction.stop: cpp_unified_action: the concrete action's control method is inherited from the unified relay::Action; it fires the `.` frame (control_id) and returns void, where Python's coroutine awaits and returns the result dict — same wire frame, C++ fire-and-forget return idiom. -signalwire.relay.event.CallReceiveEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CallReceiveEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CallStateEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CallStateEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CallingErrorEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CallingErrorEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CollectEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.CollectEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ConferenceEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ConferenceEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ConnectEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ConnectEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DenoiseEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DenoiseEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DetectEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DetectEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DialEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.DialEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.EchoEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.EchoEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.FaxEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.FaxEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.HoldEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.HoldEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.MessageReceiveEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.MessageReceiveEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.MessageStateEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.MessageStateEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.PayEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.PayEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.PlayEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.PlayEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.QueueEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.QueueEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.RecordEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.RecordEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ReferEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.ReferEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.RelayEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.RelayEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.SendDigitsEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.SendDigitsEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.StreamEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.StreamEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.TapEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.TapEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.TranscribeEvent.__init__: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.TranscribeEvent.from_payload: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). -signalwire.relay.event.parse_event: cpp_typed_event_ctor: C++ typed RELAY events construct from a single nlohmann::json payload (from_payload(json)/ctor), where Python spreads the decoded event fields as typed __init__ params; same wire event, C++ passes the raw JSON object (the port's typed-event idiom). +signalwire.relay.event.parse_event: cpp_free_function_not_enumerated: parse_event IS implemented in C++ with a matching signature — `RelayEvent parse_event(const json& payload)` at include/signalwire/relay/typed_events.hpp:558, dispatching on event_type exactly as the reference does (relay/event.py:634-638) — but the libclang SIGNATURE enumerator does not record namespace-scope free functions, so the gate reports it missing-port. Not a capability gap and not an idiom difference: an enumerator blind spot. Fix is to project namespace-scope free functions in the signature enumerator, not to change the port. signalwire.relay.message.Message.on: cpp_typed_callback: the C++ Message.on binds a typed callable<[Message],void> handler where the Python reference records callable<[RelayEvent],any>; same message-event subscription, the concrete handler element type is the C++ typed-callback idiom. signalwire.rest._base.CrudWithAddresses.__init__: cpp_crud_idiom: generated CrudWithAddresses base method takes the C++ typed params object where Python spreads **kwargs; same REST wire shape (documented CRUD idiom). signalwire.skills.registry.SkillRegistry.get_skill_class: cpp_return_idiom: C++ get_skill_class returns bool (whether the skill factory is known) where Python returns the skill type object; C++ has no first-class type value — use create() to instantiate (same discovery-by-name contract). -signalwire.web.web_service.WebService.__init__: cpp_kwargs_positional: WebService ctor/start collapse Python's many keyword options (directories/basic_auth/allowed_extensions/ssl_cert/...) into positional C++ params / accessors; same static-file service behavior (mirrors Java WebService). signalwire.web.web_service.WebService.start: cpp_kwargs_positional: WebService ctor/start collapse Python's many keyword options (directories/basic_auth/allowed_extensions/ssl_cert/...) into positional C++ params / accessors; same static-file service behavior (mirrors Java WebService). ## KNOWN PRE-EXISTING RESIDUAL (NOT item H/I) — gen-payload SWML AI-payload structs @@ -555,8 +429,6 @@ UNTAGGED on purpose — an honest gate failure, not silenced with a blanket allowlist. Fix requires the signature enumerator to project POD-struct fields under `swml_verbs_generated` as property-getters (or the port to expose them as accessors). -signalwire.rest._request_options.RequestOptions.__init__: cpp_constructor_default_only: RequestOptions is an aggregate struct with public data fields (timeout/retries/retry_on_status/retry_backoff/abort_signal); Python's dataclass __init__ enumerates each field as a keyword. The full set is reachable via the C++ public fields (ro.retries = 1, ro.abort_signal = &flag) — same callable contract, aggregate-init idiom instead of a keyword ctor (go/ts/ruby/java value-struct match). -signalwire.rest._request_options.RequestOptions.abort_signal: cpp_field_not_property: Python exposes abort_signal as a @property getter *method*; C++ implements it as a public data member (std::atomic* abort_signal) the libclang enumerator does not emit as a method. Reachable directly as ro.abort_signal — same callable contract, public-field idiom (the RequestOptions data fields are deliberately not surface symbols, exactly as the Python dataclass fields aren't). ## A-fold / G-fold signature re-key # The surface allow-lists re-key these to the folded agentbase-family. / canonical @@ -572,7 +444,6 @@ signalwire.core.agent_base.AgentBase.session_manager: agentbase_port_helper: por signalwire.core.agent_base.AgentBase.set_auth: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_auth in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. signalwire.core.agent_base.AgentBase.set_name: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_name in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. signalwire.core.agent_base.AgentBase.set_post_prompt_url_direct: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_post_prompt_url_direct in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. -signalwire.core.agent_base.AgentBase.set_signing_key: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_signing_key in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. signalwire.core.agent_base.AgentBase.set_use_pom: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_use_pom in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. signalwire.core.agent_base.AgentBase.set_webhook_url: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.set_webhook_url in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. signalwire.core.agent_base.AgentBase.supported_internal_filler_names: agentbase_port_helper: port-only C++ AgentBase member (composition-delegate or typed helper) with no Python reference twin; documented as an addition under agentbase-family.supported_internal_filler_names in PORT_ADDITIONS.md (ALLOWLIST_DISCIPLINE §4c). Re-keyed here for the signature side. @@ -583,8 +454,6 @@ signalwire.core.swml_service.SWMLService.switch: cpp_typed_verb: C++ swml::Servi signalwire.core.agent_base.AgentBase.skill_manager: cpp_private_composition: Python AgentBase.skill_manager is a @property exposing the SkillManager instance; C++ composes it privately and surfaces the operations as AgentBase methods (add_skill/remove_skill/list_skills/has_skill). Documented under agentbase-family.skill_manager in PORT_OMISSIONS.md. signalwire.core.mixins.tool_mixin.ToolMixin.tool: cpp_no_decorator: Python @tool decorator method relies on the decorator protocol; C++ has no method-decorator feature — tools register via define_tool(...). Documented under agentbase-family.tool in PORT_OMISSIONS.md (Java/TS/PHP omit likewise). signalwire.core.mixins.web_mixin.WebMixin.get_app: cpp_no_framework_app: returns a FastAPI/Flask app object; C++ runs httplib directly with no app object to return. Documented under agentbase-family.get_app in PORT_OMISSIONS.md (Java/TS/PHP omit likewise). -signalwire.pom.pom.PromptObjectModel.sections: cpp_field_not_property: Python PromptObjectModel.sections is a @property; C++ implements it as a public std::vector
data member (surfaced on the surface side, libclang emits no getter method). Reachable directly as pom.sections — public-collection idiom. -signalwire.pom.pom.Section.subsections: cpp_field_not_property: Python Section.subsections is a @property; C++ implements it as a public std::vector
data member (surfaced on the surface side, libclang emits no getter method). Reachable directly as section.subsections — public-collection idiom. # ---- dual-gate: dead for SURFACE-DIFF (A-fold/G-fold/folded types drop these as # surface symbols), but LIVE for this signature DRIFT gate — the signature gate has @@ -629,4 +498,3 @@ signalwire.core.swml_service.SWMLService.unset: cpp_typed_verb: C++ swml::Servic signalwire.core.swml_service.SWMLService.user_event: cpp_typed_verb: C++ swml::Service exposes every SWML verb as a typed method; Python routes them via __getattr__ dispatch. signalwire.rest._base.CrudResource.__init__: cpp_base_ctor: the C++ hand base CrudResource (base_resource.hpp, routed to rest._base) carries an explicit constructor (HttpClient receiver + composed base path); Python's CrudResource inherits __init__ up its base chain and griffe records no own __init__ on it. Same construction contract, C++ declares the ctor on each base per its inheritance idiom. signalwire.rest._base.ReadResource.__init__: cpp_base_ctor: the C++ hand base ReadResource (base_resource.hpp, routed to rest._base) carries an explicit constructor (receiver + composed base path); Python's ReadResource inherits __init__ from BaseResource and griffe records no own __init__ on ReadResource. Same construction contract, C++ declares the ctor on each base per its inheritance idiom. -signalwire.agent_server.AgentServer.app: cpp_only: Python AgentServer.app exposes the underlying Flask/FastAPI app for advanced HTTP integration; C++ AgentServer uses httplib directly with no analogous app object to surface. End users reach HTTP customization via on_request / on_swml_request / register_route hooks instead. (Signature missing-port: the surface oracle omits `app`, so its former SURFACE omission was dead and was removed; the signature oracle records it, so it is excused here.) diff --git a/README.md b/README.md index 9816add..10d1219 100644 --- a/README.md +++ b/README.md @@ -45,32 +45,40 @@ Each agent is a self-contained microservice that generates [SWML](docs/swml_serv ```cpp -#include #include +#include +#include using namespace signalwire; using json = nlohmann::json; class MyAgent : public agent::AgentBase { -public: - MyAgent() : AgentBase("my-agent", "/agent") { - add_language({"English", "en-US", "inworld.Mark"}); - prompt_add_section("Role", "You are a helpful assistant."); - - define_tool("get_time", "Get the current time", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& /*args*/, const json& /*raw*/) -> swaig::FunctionResult { - auto now = std::time(nullptr); - char buf[32]; - std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&now)); - return swaig::FunctionResult(std::string("The time is ") + buf); - }); - } + public: + MyAgent() : AgentBase("my-agent", "/agent") { + add_language({"English", "en-US", "inworld.Mark"}); + prompt_add_section("Role", "You are a helpful assistant."); + + define_tool("get_time", "Get the current time", + {{"type", "object"}, {"properties", json::object()}}, + [](const json& /*args*/, const json& /*raw*/) -> swaig::FunctionResult { + auto now = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&now)); + return swaig::FunctionResult(std::string("The time is ") + buf); + }); + } }; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { MyAgent agent; agent.run(); // Serves on http://0.0.0.0:3000/agent + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } ``` @@ -124,27 +132,24 @@ Real-time call control and messaging over WebSocket. The RELAY client connects t ```cpp -#include - #include +#include using namespace signalwire::relay; int main() { - auto client = RelayClient::from_env(); - - client.on_call([](Call& call) { - call.answer(); - auto action = call.play({ - {{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}} - }); - if (!action.wait()) { // false = call ended before playback finished - std::cerr << "playback interrupted\n"; - } - call.hangup(); - }); + auto client = RelayClient::from_env(); - client.run(); + client.on_call([](Call& call) { + call.answer(); + auto action = call.play({{{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}}}); + if (!action.wait()) { // false = call ended before playback finished + std::cerr << "playback interrupted\n"; + } + call.hangup(); + }); + + client.run(); } ``` @@ -163,23 +168,32 @@ Synchronous REST client for managing SignalWire resources and controlling calls ```cpp +#include #include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { auto client = RestClient::from_env(); auto agents = client.fabric().ai_agents.list(); - auto call = client.calling().dial({ - .from = "+15559876543", .to = "+15551234567", + auto call = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", .url = "https://example.com/handler", }); auto numbers = client.phone_numbers().search({{"areacode", "512"}}); auto results = client.datasphere().documents.search({ .query_string = "billing policy", }); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } ``` diff --git a/ROOT_HYGIENE_ALLOW.md b/ROOT_HYGIENE_ALLOW.md index ccf18e3..3939787 100644 --- a/ROOT_HYGIENE_ALLOW.md +++ b/ROOT_HYGIENE_ALLOW.md @@ -30,3 +30,4 @@ Format: `- — reason (approver, date)`. - WIRED_MODES.md — WIRED-MODES gate manifest declaring the load-bearing run-ci env/mode lines, read by porting-sdk check_wired_modes.py at repo root (plan 1.6/D7, lane-cpp, 2026-07-19) - .doc_surface_floor — DOC-SURFACE doxygen-header coverage floor pin, read + ratcheted at repo root by porting-sdk doc_surface.py (plan 6.3, lane-cpp, 2026-07-19) - port_surface_native.json — required audit-contract file: the shared porting-sdk consumers look for it AT THE REPO ROOT by that exact name (suites/_doc_audit.py resolves `repo_path / "port_surface_native.json"`, then hands it to audit_docs.py --native-names), so it cannot move to eng/. Holds cpp's own member spellings (set_route / set_port / set_venue_name / …) so DOC-AUDIT can resolve this port's correct, compiling doc examples after the accessor fold. Denied from published packages via ARTIFACT_DENY_ALLOW.md, so it never ships. (green-cpp lane, 2026-07-26) +- ruff.toml — the PY-LINT gate's tool config. ruff discovers its configuration by walking UP from the files it lints, so a `ruff.toml` under `eng/` would never be found when linting `scripts/*.py`; root is where the tool looks, the same convention `.clang-tidy` and `.clang-format` already follow here. Mirrors the reference implementation's ruff rule selection (signalwire-python/pyproject.toml). (lint-cpp lane, 2026-07-30) diff --git a/WIRED_MODES.md b/WIRED_MODES.md index 7649a70..4ac6e50 100644 --- a/WIRED_MODES.md +++ b/WIRED_MODES.md @@ -15,3 +15,4 @@ Prose/headers/comments are ignored, so this file doubles as human documentation. - `MOCK_RELAY_STRICT=1` — RELAY strict mode: the RELAY mock suite re-runs with the shared mock in 400-on-violation mode (unknown field / duplicate id) so a wire-shape regression the tolerant mock would swallow fails loud (STRICT-MOCKS gate; the env is inherited by the fork+execlp'd `python -m mock_relay` child). - `strict_mocks_gate` — nightly strict RELAY re-run body: the function the STRICT-MOCKS gate line invokes (with cpp's host/exec/run BUILD_MODE routing). This exact body was silently deleted in the strict-mocks × Part-5 merge race (call survived, body gone → nightly exit 127); this pattern pins body + call. - `export MOCK_SIGNALWIRE_STRICT` — REST 400 strict default (D3): the REST mock returns 400 on an unknown key / wrong type instead of tolerantly journaling it, exported run-ci-wide so the TEST + REST-COVERAGE lanes catch the regression; inherited by the mocktest harness's spawned `python -m mock_signalwire`. +- `pylint_gate` — PY-LINT body: the function the PY-LINT gate line invokes (dual-mode, LOCAL applies / CI `--check`). Same shape the strict-mocks merge race destroyed once already — a call line surviving a dropped body would make the gate exit 127 instead of linting the 9 Python files under `scripts/`, two of which (`_cpp_fmt.py`, `clang_tidy_cache.py`) are the very lint/format infrastructure the FMT and LINT gates run through. This pattern pins body + call. diff --git a/docs/agent_guide.md b/docs/agent_guide.md index 30f9f38..d0ffbe0 100644 --- a/docs/agent_guide.md +++ b/docs/agent_guide.md @@ -157,7 +157,7 @@ You can also nest a subsection under an existing section, or append to a section ```cpp agent.prompt_add_subsection("Instructions", "Escalation", "When to escalate to a human agent.", - {"After two failed attempts", "On explicit request"}); + std::vector{"After two failed attempts", "On explicit request"}); agent.prompt_add_to_section("Instructions", "Always confirm the caller's identity first."); ``` @@ -939,7 +939,7 @@ AgentBase(const std::string& name = "agent", - `set_prompt_text(text)` / `set_use_pom(bool)` / `set_prompt_pom(vector)` - `set_post_prompt(text)` / `set_post_prompt_url(url)` - `prompt_add_section(title, body = "", bullets = {})` -- `prompt_add_subsection(parent_title, title, body = "", bullets = {})` +- `prompt_add_subsection(parent_title, title, body = "", bullets = std::nullopt)` - `prompt_add_to_section(title, body = "", bullets = {})` ### SWAIG / Tool Methods diff --git a/docs/api_reference.md b/docs/api_reference.md index 13eb090..149e649 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -241,10 +241,9 @@ agent.prompt_add_to_section("Process", "", {"Follow up", "Close ticket"}); ##### `prompt_add_subsection` ```cpp -signalwire::agent::AgentBase& prompt_add_subsection(const std::string& parent_title, - const std::string& title, - const std::string& body = "", - const std::vector& bullets = {}); +signalwire::agent::AgentBase& prompt_add_subsection( + const std::string& parent_title, const std::string& title, const std::string& body = "", + const std::optional>& bullets = std::nullopt); ``` Add a subsection to an existing prompt section. @@ -252,13 +251,14 @@ Add a subsection to an existing prompt section. - `parent_title` (`std::string`): Title of the parent section - `title` (`std::string`): Subsection title - `body` (`std::string`): Subsection content (default: `""`) -- `bullets` (`std::vector`): Subsection bullet points (default: empty) +- `bullets` (`std::optional>`): Subsection bullet points + (default: `std::nullopt`, matching the reference's `bullets: list[str] | None = None`) **Usage:** ```cpp agent.prompt_add_subsection("Guidelines", "Escalation Rules", "Escalate when:", - {"Customer is angry", "Technical issue beyond scope"}); + std::vector{"Customer is angry", "Technical issue beyond scope"}); ``` ### Voice and Language Configuration @@ -1840,7 +1840,7 @@ result.join_conference("support_conference", opts); ### Payment Processing -##### `pay(const std::string& payment_connector_url, const std::string& input_method = "dtmf", const std::string& status_url = "", const std::string& payment_method = "credit-card", int timeout = 5, int max_attempts = 1, bool security_code = true, const std::string& postal_code = "true", int min_postal_code_length = 0, const std::string& token_type = "reusable", const std::string& charge_amount = "", const std::string& currency = "usd", const std::string& language = "en-US", const std::string& voice = "woman", const std::string& description = "", const std::string& valid_card_types = "visa mastercard amex", const std::vector& parameters = {}, const std::vector& prompts = {}) -> FunctionResult&` +##### `pay(const std::string& payment_connector_url, const std::string& input_method = "dtmf", const std::string& status_url = "", const std::string& payment_method = "credit-card", int timeout = 5, int max_attempts = 1, bool security_code = true, const std::variant& postal_code = true, int min_postal_code_length = 0, const std::string& token_type = "reusable", const std::string& charge_amount = "", const std::string& currency = "usd", const std::string& language = "en-US", const std::string& voice = "woman", const std::string& description = "", const std::string& valid_card_types = "visa mastercard amex", const std::vector& parameters = {}, const std::vector& prompts = {}) -> FunctionResult&` Process a payment through the call. **Parameters:** @@ -1851,7 +1851,7 @@ Process a payment through the call. - `timeout` (`int`): Input timeout in seconds (default: 5) - `max_attempts` (`int`): Maximum retry attempts (default: 1) - `security_code` (`bool`): Require security code (default: `true`) -- `postal_code` (`std::string`): Require postal code (default: "true") +- `postal_code` (`std::variant`): Whether to prompt for a postal code, or an actual postcode (default: `true`). A bool is emitted as the lowercase string `"true"`/`"false"`; a string is passed through verbatim. - `min_postal_code_length` (`int`): Minimum postal code length (default: 0) - `token_type` (`std::string`): Token type: "reusable", "one-time" (default: "reusable") - `charge_amount` (`std::string`): Amount to charge @@ -2176,34 +2176,9 @@ data_map.webhook( json{{"Authorization", "Bearer YOUR_API_TOKEN"}}); // Use static credentials ``` -##### `body(const json& data) -> DataMap&` -Set the JSON body for the last-added webhook (POST/PUT requests). - -**Parameters:** -- `data` (`json`): JSON body data (supports `${variable}` substitution) - -**Usage:** -```cpp -// Static body with parameter substitution -data_map.body({ - {"query", "${args.search_term}"}, - {"limit", 5}, - {"filters", { - {"category", "${args.category}"}, - {"active", true} - }} -}); - -// Body with call-related data (NOT sensitive info) -data_map.body({ - {"customer_id", "${global_data.customer_id}"}, - {"request_id", "${meta_data.call_id}"}, - {"search", "${args.query}"} -}); -``` - ##### `params(const json& data) -> DataMap&` -Set request params for the last-added webhook (alias for `body`). +Set request params for the last-added webhook — including the data sent with +POST/PUT requests. **Parameters:** - `data` (`json`): Query parameters (supports `${variable}` substitution) @@ -2460,7 +2435,7 @@ auto search_tool = signalwire::datamap::DataMap("search_knowledge") "POST", "https://api.company.com/search", json{{"Authorization", "Bearer TOKEN"}}) - .body({ + .params({ {"query", "${args.query}"}, {"category", "${args.category}"}, {"limit", 5} diff --git a/docs/security.md b/docs/security.md index 5fc862e..52f2e59 100644 --- a/docs/security.md +++ b/docs/security.md @@ -53,6 +53,21 @@ self-signed CA, point the transport at a PEM bundle. Certificate verification is | `SIGNALWIRE_RELAY_CA_FILE` | *system trust store* | Path to a PEM CA bundle the RELAY WebSocket client trusts for `wss://` connections. `SSL_CERT_FILE` is a secondary fallback. | | `SIGNALWIRE_RELAY_PING_INTERVAL_SECS` | `30` | RELAY WebSocket ping-heartbeat interval (seconds). The client pings the peer at this interval and, absent a pong, closes the socket so a half-open peer is detected and reconnection kicks in. A value ≤ 0 or malformed is ignored (keeps the default). | +#### No silent downgrade to plaintext + +`SIGNALWIRE_RELAY_CA_FILE` is an explicit request to **verify** the RELAY peer, +which a plaintext transport can never honour. If it is set while the RELAY +transport resolves to plain `ws://` — a stale `SIGNALWIRE_RELAY_SCHEME`, a test +harness export leaking into a real run, an operator who changed one setting and +not the other — `RelayClient::connect()` **refuses and returns `false`**, logging +which setting would otherwise have been silently ignored. It does not complete an +unencrypted session behind a caller who asked for encryption. + +Plaintext *without* that variable is unaffected: `SIGNALWIRE_RELAY_SCHEME=ws` +alone is an unambiguous request for a clear connection (the audit fixture and dev +servers) and still works. Unset `SIGNALWIRE_RELAY_CA_FILE` to connect in the +clear deliberately. + ### Authentication | Variable | Default | Description | diff --git a/docs/swml_service_guide.md b/docs/swml_service_guide.md index 2559413..81436f4 100644 --- a/docs/swml_service_guide.md +++ b/docs/swml_service_guide.md @@ -268,8 +268,10 @@ class VipVoiceService : public signalwire::swml::Service { protected: std::optional on_swml_request( const std::optional& request_data = std::nullopt, - const std::optional& callback_path = std::nullopt) override { + const std::optional& callback_path = std::nullopt, + const std::optional& request = std::nullopt) override { (void)callback_path; + (void)request; if (!request_data) { return std::nullopt; } @@ -520,7 +522,7 @@ returns `Service&` for chaining): - `serve()`: Start the HTTP server (blocking) - `stop()`: Stop the HTTP server - `get_basic_auth_credentials()` / `get_basic_auth_credentials_with_source()`: Get the basic-auth credentials -- `on_swml_request(request_data, callback_path)`: Called when SWML is requested +- `on_swml_request(request_data, callback_path, request)`: Called when SWML is requested - `register_routing_callback(callback_fn, path)`: Register a callback for request routing ### Verb Helper Methods @@ -597,8 +599,10 @@ class CallRouterService : public signalwire::swml::Service { protected: std::optional on_swml_request( const std::optional& request_data = std::nullopt, - const std::optional& callback_path = std::nullopt) override { + const std::optional& callback_path = std::nullopt, + const std::optional& request = std::nullopt) override { (void)callback_path; + (void)request; // If there's no request data, use default routing. if (!request_data) { get_logger().debug("no_request_data_using_default"); diff --git a/examples/advanced_datamap_demo.cpp b/examples/advanced_datamap_demo.cpp index c6f4b10..ca1d67b 100644 --- a/examples/advanced_datamap_demo.cpp +++ b/examples/advanced_datamap_demo.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2025 SignalWire — MIT License // Advanced DataMap patterns: multi-webhook, foreach, expression, fallback. +#include #include #include @@ -8,39 +9,47 @@ using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("advanced-datamap", "/advanced-datamap"); agent.prompt_add_section("Role", "You have advanced data_map tools."); // Multi-webhook with fallback - auto search = datamap::DataMap("multi_search") - .description("Search with fallback APIs") - .parameter("query", "string", "Search query", true) - .parameter("priority", "string", "fast or comprehensive", false, - {"fast", "comprehensive"}) - .webhook("GET", "https://api.fast.com/q?term=${args.query}", - {{"X-API-Key", "FAST_KEY"}}) - .webhook("GET", "https://api.fallback.com/search?q=${args.query}", - {{"Authorization", "Bearer TOKEN"}}) - .foreach({{"input_key", "${response.items}"}, {"output_key", "foreach"}, {"append", true}}) - .output(swaig::FunctionResult("Result: ${foreach.title} - Score: ${foreach.relevance}")) - .error_keys({"error", "failed"}); + auto search = + datamap::DataMap("multi_search") + .description("Search with fallback APIs") + .parameter("query", "string", "Search query", true) + .parameter("priority", "string", "fast or comprehensive", false, + {"fast", "comprehensive"}) + .webhook("GET", "https://api.fast.com/q?term=${args.query}", + {{"X-API-Key", "FAST_KEY"}}) + .webhook("GET", "https://api.fallback.com/search?q=${args.query}", + {{"Authorization", "Bearer TOKEN"}}) + .foreach ( + {{"input_key", "${response.items}"}, {"output_key", "foreach"}, {"append", true}}) + .output(swaig::FunctionResult("Result: ${foreach.title} - Score: ${foreach.relevance}")) + .error_keys({"error", "failed"}); agent.register_swaig_function(search.to_swaig_function()); // Expression-based routing - auto router = datamap::DataMap("route_request") - .description("Route requests by type") - .parameter("type", "string", "Request type", true) - .expression("${args.type}", "billing.*", - swaig::FunctionResult("Routing to billing department") - .connect("+15551001", true)) - .expression("${args.type}", "tech.*", - swaig::FunctionResult("Routing to tech support") - .connect("+15551002", true)) - .expression("${args.type}", "sales.*", - swaig::FunctionResult("Routing to sales") - .connect("+15551003", true)); + auto router = + datamap::DataMap("route_request") + .description("Route requests by type") + .parameter("type", "string", "Request type", true) + .expression( + "${args.type}", "billing.*", + swaig::FunctionResult("Routing to billing department").connect("+15551001", true)) + .expression("${args.type}", "tech.*", + swaig::FunctionResult("Routing to tech support").connect("+15551002", true)) + .expression("${args.type}", "sales.*", + swaig::FunctionResult("Routing to sales").connect("+15551003", true)); agent.register_swaig_function(router.to_swaig_function()); std::cout << "Advanced DataMap at http://0.0.0.0:3000/advanced-datamap\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/auto_vivified_example.cpp b/examples/auto_vivified_example.cpp index 6357e65..4768dab 100644 --- a/examples/auto_vivified_example.cpp +++ b/examples/auto_vivified_example.cpp @@ -1,28 +1,31 @@ // Copyright (c) 2025 SignalWire — MIT License // Auto-built SWML services: voicemail, IVR, and call transfer. -#include #include +#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { // --- Voicemail Service --- swml::Service voicemail; voicemail.set_route("/voicemail"); voicemail.answer(); - voicemail.play({{"url", "say:Hello, you have reached the voicemail service. Please leave a message after the beep."}}); + voicemail.play({{"url", + "say:Hello, you have reached the voicemail service. Please leave a message " + "after the beep."}}); voicemail.sleep(1000); voicemail.play({{"url", "https://example.com/beep.wav"}}); - voicemail.record({ - {"format", "mp3"}, - {"stereo", false}, - {"beep", false}, - {"max_length", 120}, - {"terminators", "#"}, - {"status_url", "https://example.com/voicemail-status"} - }); + voicemail.record({{"format", "mp3"}, + {"stereo", false}, + {"beep", false}, + {"max_length", 120}, + {"terminators", "#"}, + {"status_url", "https://example.com/voicemail-status"}}); voicemail.play({{"url", "say:Thank you for your message. Goodbye!"}}); voicemail.hangup(); @@ -31,11 +34,9 @@ int main() { ivr.set_route("/ivr"); ivr.answer(); - ivr.prompt({ - {"play", "say:Press 1 for sales, 2 for support."}, - {"max_digits", 1}, - {"terminators", "#"} - }); + ivr.prompt({{"play", "say:Press 1 for sales, 2 for support."}, + {"max_digits", 1}, + {"terminators", "#"}}); ivr.transfer({{"dest", "main_menu"}}); // --- Call Transfer Service --- @@ -44,18 +45,18 @@ int main() { transfer.answer(); transfer.play({{"url", "say:Connecting you with the next available agent."}}); - transfer.connect({ - {"from", "+15551234567"}, - {"timeout", 30}, - {"parallel", nlohmann::json::array({ - {{"to", "+15552223333"}}, - {{"to", "+15554445555"}} - })} - }); + transfer.connect({{"from", "+15551234567"}, + {"timeout", 30}, + {"parallel", nlohmann::json::array( + {{{"to", "+15552223333"}}, {{"to", "+15554445555"}}})}}); transfer.record({{"format", "mp3"}, {"beep", true}, {"max_length", 120}}); transfer.hangup(); std::cout << "Voicemail SWML:\n" << voicemail.render_swml().dump(2) << "\n"; std::cout << "Starting voicemail service at http://0.0.0.0:3000/voicemail\n"; voicemail.serve(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/basic_swml_service.cpp b/examples/basic_swml_service.cpp index 1cade85..4d715a5 100644 --- a/examples/basic_swml_service.cpp +++ b/examples/basic_swml_service.cpp @@ -24,53 +24,54 @@ // Probe: // curl -u user:pass http://localhost:3000/swml -#include - #include #include +#include #include using namespace signalwire; using json = nlohmann::json; static void build_voicemail(swml::Service& svc) { - svc.answer(); - svc.play(json{{"url", "https://cdn.signalwire.com/voicemail/greeting.mp3"}}); - svc.record(json{ - {"format", "mp4"}, - {"stereo", true}, - {"max_length", 120}, - }); - svc.hangup(); + svc.answer(); + svc.play(json{{"url", "https://cdn.signalwire.com/voicemail/greeting.mp3"}}); + svc.record(json{ + {"format", "mp4"}, + {"stereo", true}, + {"max_length", 120}, + }); + svc.hangup(); } static void build_ivr(swml::Service& svc) { - svc.answer(); - svc.play(json{{"url", "https://cdn.signalwire.com/ivr/menu.mp3"}}); - svc.send_digits(json{{"digits", "1"}}); // demo placeholder - svc.transfer(json{{"dest", "+15555555555"}}); - svc.hangup(); + svc.answer(); + svc.play(json{{"url", "https://cdn.signalwire.com/ivr/menu.mp3"}}); + svc.send_digits(json{{"digits", "1"}}); // demo placeholder + svc.transfer(json{{"dest", "+15555555555"}}); + svc.hangup(); } static void build_transfer(swml::Service& svc) { - svc.answer(); - svc.play(json{{"url", "https://cdn.signalwire.com/transfer/please_hold.mp3"}}); - svc.transfer(json{{"dest", "sip:agent@example.com"}}); + svc.answer(); + svc.play(json{{"url", "https://cdn.signalwire.com/transfer/please_hold.mp3"}}); + svc.transfer(json{{"dest", "sip:agent@example.com"}}); } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { swml::Service svc; - svc.set_name("basic-swml") - .set_route("/swml"); + svc.set_name("basic-swml").set_route("/swml"); const char* flow = std::getenv("FLOW"); std::string which = flow ? flow : "voicemail"; if (which == "ivr") { - build_ivr(svc); + build_ivr(svc); } else if (which == "transfer") { - build_transfer(svc); + build_transfer(svc); } else { - build_voicemail(svc); + build_voicemail(svc); } std::cout << "Basic SWMLService — flow: " << which << "\n"; @@ -81,4 +82,8 @@ int main() { svc.serve(); return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/call_flow_and_actions_demo.cpp b/examples/call_flow_and_actions_demo.cpp index 99eff27..ae61187 100644 --- a/examples/call_flow_and_actions_demo.cpp +++ b/examples/call_flow_and_actions_demo.cpp @@ -1,12 +1,16 @@ // Copyright (c) 2025 SignalWire — MIT License // Call flow with verb pipeline (pre-answer, answer, post-answer, post-AI). +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("call-flow", "/call-flow"); agent.prompt_add_section("Role", "You are a call center agent."); @@ -14,30 +18,32 @@ int main() { // 5-phase verb pipeline agent.add_pre_answer_verb("play", {{"url", "https://example.com/ringtone.mp3"}}); agent.add_answer_verb("answer", {{"max_duration", 3600}}); - agent.add_post_answer_verb("record_call", { - {"stereo", true}, {"format", "wav"} - }); + agent.add_post_answer_verb("record_call", {{"stereo", true}, {"format", "wav"}}); agent.add_post_ai_verb("hangup", json::object()); // Tools with call control actions - agent.define_tool("transfer_call", "Transfer to another number", - {{"type", "object"}, {"properties", { - {"number", {{"type", "string"}, {"description", "Phone number"}}} - }}}, + agent.define_tool( + "transfer_call", "Transfer to another number", + {{"type", "object"}, + {"properties", {{"number", {{"type", "string"}, {"description", "Phone number"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string num = args.value("number", ""); - return swaig::FunctionResult("Transferring to " + num) - .connect(num, true); + (void)raw; + std::string num = args.value("number", ""); + return swaig::FunctionResult("Transferring to " + num).connect(num, true); }); agent.define_tool("end_call", "End the current call", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Goodbye!").hangup(); - }); + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + return swaig::FunctionResult("Goodbye!").hangup(); + }); std::cout << "Call flow demo at http://0.0.0.0:3000/call-flow\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/comprehensive_dynamic_agent.cpp b/examples/comprehensive_dynamic_agent.cpp index 3661a62..92ebff7 100644 --- a/examples/comprehensive_dynamic_agent.cpp +++ b/examples/comprehensive_dynamic_agent.cpp @@ -1,12 +1,16 @@ // Copyright (c) 2025 SignalWire — MIT License // Comprehensive dynamic agent with per-request customization. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("dynamic-full", "/dynamic-full"); agent.prompt_add_section("Role", "You are a configurable multi-tenant assistant."); @@ -15,39 +19,43 @@ int main() { agent.add_skill("math"); agent.set_dynamic_config_callback( - [](const std::map& query, - const json& body, const std::map& headers, - agent::AgentBase& copy) { - (void)body; (void)headers; - - // Tenant customization - auto t = query.find("tenant"); - if (t != query.end()) { - copy.set_name(t->second + " Assistant"); - copy.prompt_add_section("Tenant", "You work for " + t->second); - copy.set_global_data({{"tenant_id", t->second}}); - } - - // Voice selection - auto v = query.find("voice"); - if (v != query.end()) { - copy.add_language({"Custom", "en-US", v->second}); - } - - // Model override - auto m = query.find("model"); - if (m != query.end()) { - copy.set_params({{"ai_model", m->second}}); - } - - // Extra instructions - auto i = query.find("instructions"); - if (i != query.end()) { - copy.prompt_add_section("Extra Instructions", i->second); - } + [](const std::map& query, const json& body, + const std::map& headers, agent::AgentBase& copy) { + (void)body; + (void)headers; + + // Tenant customization + auto t = query.find("tenant"); + if (t != query.end()) { + copy.set_name(t->second + " Assistant"); + copy.prompt_add_section("Tenant", "You work for " + t->second); + copy.set_global_data({{"tenant_id", t->second}}); + } + + // Voice selection + auto v = query.find("voice"); + if (v != query.end()) { + copy.add_language({"Custom", "en-US", v->second}); + } + + // Model override + auto m = query.find("model"); + if (m != query.end()) { + copy.set_params({{"ai_model", m->second}}); + } + + // Extra instructions + auto i = query.find("instructions"); + if (i != query.end()) { + copy.prompt_add_section("Extra Instructions", i->second); + } }); std::cout << "Comprehensive dynamic at http://0.0.0.0:3000/dynamic-full\n"; std::cout << "Try: ?tenant=Acme&voice=inworld.Sarah&model=gpt-4.1-nano\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/concierge_agent_example.cpp b/examples/concierge_agent_example.cpp index 22e632b..f47ae33 100644 --- a/examples/concierge_agent_example.cpp +++ b/examples/concierge_agent_example.cpp @@ -1,26 +1,39 @@ // Copyright (c) 2025 SignalWire — MIT License // Concierge prefab: venue information assistant. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { prefabs::ConciergeAgent agent("hotel-concierge", "/concierge"); agent.set_venue_name("Grand Hotel"); - agent.set_amenities({ - {{"name", "Pool"}, {"description", "Rooftop infinity pool with cabanas"}, {"hours", "6AM-10PM"}}, - {{"name", "Spa"}, {"description", "Full-service spa with sauna and steam room"}, {"hours", "8AM-8PM"}}, - {{"name", "Restaurant"}, {"description", "Fine dining with panoramic views"}, {"hours", "7AM-11PM"}}, - {{"name", "Fitness Center"}, {"description", "State-of-the-art gym"}, {"hours", "24/7"}} - }); - agent.set_hours({ - {"check_in", "3:00 PM"}, {"check_out", "11:00 AM"}, - {"front_desk", "24/7"}, {"valet", "24/7"} - }); + agent.set_amenities( + {{{"name", "Pool"}, + {"description", "Rooftop infinity pool with cabanas"}, + {"hours", "6AM-10PM"}}, + {{"name", "Spa"}, + {"description", "Full-service spa with sauna and steam room"}, + {"hours", "8AM-8PM"}}, + {{"name", "Restaurant"}, + {"description", "Fine dining with panoramic views"}, + {"hours", "7AM-11PM"}}, + {{"name", "Fitness Center"}, {"description", "State-of-the-art gym"}, {"hours", "24/7"}}}); + agent.set_hours({{"check_in", "3:00 PM"}, + {"check_out", "11:00 AM"}, + {"front_desk", "24/7"}, + {"valet", "24/7"}}); std::cout << "Concierge at http://0.0.0.0:3000/concierge\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/contexts_demo.cpp b/examples/contexts_demo.cpp index f6b5eec..d245294 100644 --- a/examples/contexts_demo.cpp +++ b/examples/contexts_demo.cpp @@ -1,18 +1,20 @@ // Copyright (c) 2025 SignalWire — MIT License // Demonstrates contexts/steps system with multi-persona workflows. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("Computer Sales", "/contexts-demo"); - agent.prompt_add_section("Instructions", - "Follow the structured sales workflow.", { - "Complete each step's criteria before advancing", - "Be helpful and consultative" - }); + agent.prompt_add_section( + "Instructions", "Follow the structured sales workflow.", + {"Complete each step's criteria before advancing", "Be helpful and consultative"}); auto& ctx = agent.define_contexts(); @@ -22,11 +24,8 @@ int main() { sales.add_section("Role", "You are Franklin, a computer sales agent."); sales.add_step("determine_use_case") .add_section("Task", "Identify the customer's primary use case") - .add_bullets("Questions", { - "What will they use the computer for?", - "Do they play games?", - "Do they need it for work?" - }) + .add_bullets("Questions", {"What will they use the computer for?", "Do they play games?", + "Do they need it for work?"}) .set_step_criteria("Customer has stated: GAMING, WORK, or BALANCED") .set_valid_steps({"determine_form_factor"}) .set_valid_contexts({"tech_support", "manager"}); @@ -68,4 +67,8 @@ int main() { std::cout << "Contexts demo at http://0.0.0.0:3000/contexts-demo\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/custom_path_agent.cpp b/examples/custom_path_agent.cpp index 292f0e2..07cdbfc 100644 --- a/examples/custom_path_agent.cpp +++ b/examples/custom_path_agent.cpp @@ -1,23 +1,32 @@ // Copyright (c) 2025 SignalWire — MIT License // Agent with a custom HTTP route path. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("custom-path", "/api/v2/my-custom-agent", "0.0.0.0", 8080); agent.prompt_add_section("Role", "You are an agent at a custom path."); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); - agent.define_tool("ping", "Respond with pong", - {{"type", "object"}, {"properties", nlohmann::json::object()}}, + agent.define_tool( + "ping", "Respond with pong", {{"type", "object"}, {"properties", nlohmann::json::object()}}, [](const nlohmann::json& args, const nlohmann::json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("pong!"); + (void)args; + (void)raw; + return swaig::FunctionResult("pong!"); }); std::cout << "Custom path agent at http://0.0.0.0:8080/api/v2/my-custom-agent\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/datamap_demo.cpp b/examples/datamap_demo.cpp index b2c97bc..5309eac 100644 --- a/examples/datamap_demo.cpp +++ b/examples/datamap_demo.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2025 SignalWire — MIT License // Demonstrates DataMap tools: server-side API calls without webhooks. +#include #include #include @@ -8,43 +9,55 @@ using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("datamap-demo", "/datamap-demo"); agent.prompt_add_section("Role", "You have data_map tools for testing."); // 1. Simple weather API - auto weather = datamap::DataMap("get_weather") - .purpose("Get current weather for a city") - .parameter("location", "string", "City name", true) - .webhook("GET", "https://api.weather.com/v1/current?key=KEY&q=${args.location}") - .output(swaig::FunctionResult( - "Weather in ${args.location}: ${response.current.condition.text}, ${response.current.temp_f}F")) - .error_keys({"error", "message"}); + auto weather = + datamap::DataMap("get_weather") + .purpose("Get current weather for a city") + .parameter("location", "string", "City name", true) + .webhook("GET", "https://api.weather.com/v1/current?key=KEY&q=${args.location}") + .output(swaig::FunctionResult( + "Weather in ${args.location}: ${response.current.condition.text}, " + "${response.current.temp_f}F")) + .error_keys({"error", "message"}); agent.register_swaig_function(weather.to_swaig_function()); // 2. Expression-based file control - auto file_ctrl = datamap::DataMap("file_control") - .description("Control audio/video playback") - .parameter("command", "string", "Playback command", true) - .parameter("filename", "string", "File to control") - .expression("${args.command}", "start.*", - swaig::FunctionResult("Starting playback") - .add_action("start_playback", {{"file", "${args.filename}"}})) - .expression("${args.command}", "stop.*", - swaig::FunctionResult("Stopping playback") - .add_action("stop_playback", true)); + auto file_ctrl = + datamap::DataMap("file_control") + .description("Control audio/video playback") + .parameter("command", "string", "Playback command", true) + .parameter("filename", "string", "File to control") + .expression("${args.command}", "start.*", + swaig::FunctionResult("Starting playback") + .add_action("start_playback", {{"file", "${args.filename}"}})) + .expression( + "${args.command}", "stop.*", + swaig::FunctionResult("Stopping playback").add_action("stop_playback", true)); agent.register_swaig_function(file_ctrl.to_swaig_function()); // 3. Knowledge search with foreach - auto search = datamap::DataMap("search_knowledge") - .description("Search knowledge base") - .parameter("query", "string", "Search query", true) - .webhook("POST", "https://api.knowledge.com/search", - {{"Authorization", "Bearer TOKEN"}, {"Content-Type", "application/json"}}) - .body({{"query", "${query}"}, {"limit", 5}}) - .foreach({{"input_key", "${response.results}"}, {"output_key", "foreach"}, {"append", true}}) - .output(swaig::FunctionResult("Found: ${foreach.title} - ${foreach.summary}")); + auto search = + datamap::DataMap("search_knowledge") + .description("Search knowledge base") + .parameter("query", "string", "Search query", true) + .webhook("POST", "https://api.knowledge.com/search", + {{"Authorization", "Bearer TOKEN"}, {"Content-Type", "application/json"}}) + .params({{"query", "${query}"}, {"limit", 5}}) + .foreach ( + {{"input_key", "${response.results}"}, {"output_key", "foreach"}, {"append", true}}) + .output(swaig::FunctionResult("Found: ${foreach.title} - ${foreach.summary}")); agent.register_swaig_function(search.to_swaig_function()); std::cout << "DataMap demo at http://0.0.0.0:3000/datamap-demo\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/datasphere.cpp b/examples/datasphere.cpp index f5ef85c..342549c 100644 --- a/examples/datasphere.cpp +++ b/examples/datasphere.cpp @@ -1,24 +1,24 @@ // Copyright (c) 2025 SignalWire — MIT License // Datasphere agent: knowledge search via SignalWire Datasphere. +#include #include #include -#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("datasphere", "/datasphere"); agent.prompt_add_section("Role", "You are a knowledge agent with Datasphere access."); - agent.prompt_add_section("Instructions", "", { - "Search the knowledge base when users ask questions", - "Provide accurate answers based on indexed documents" - }); + agent.prompt_add_section("Instructions", "", + {"Search the knowledge base when users ask questions", + "Provide accurate answers based on indexed documents"}); - agent.add_skill("datasphere", { - {"document_id", signalwire::get_env("DATASPHERE_DOCUMENT_ID")} - }); + agent.add_skill("datasphere", {{"document_id", signalwire::get_env("DATASPHERE_DOCUMENT_ID")}}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); agent.add_language({"English", "en-US", "inworld.Mark"}); @@ -26,4 +26,8 @@ int main() { std::cout << "Datasphere agent at http://0.0.0.0:3000/datasphere\n"; std::cout << "Requires: SIGNALWIRE_PROJECT_ID, SIGNALWIRE_API_TOKEN, DATASPHERE_DOCUMENT_ID\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/datasphere_multi_instance_demo.cpp b/examples/datasphere_multi_instance_demo.cpp index c4cf112..990a865 100644 --- a/examples/datasphere_multi_instance_demo.cpp +++ b/examples/datasphere_multi_instance_demo.cpp @@ -1,16 +1,20 @@ // Copyright (c) 2025 SignalWire — MIT License // DataSphere skill with multiple instances and custom tool names. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("multi-datasphere", "/datasphere-multi"); agent.prompt_add_section("Role", - "You are an assistant with access to multiple knowledge bases. " - "Use the appropriate search tool depending on the topic."); + "You are an assistant with access to multiple knowledge bases. " + "Use the appropriate search tool depending on the topic."); agent.add_language({"English", "en-US", "inworld.Mark"}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); @@ -19,30 +23,27 @@ int main() { agent.add_skill("math", {}); // Instance 1: Drinks knowledge - agent.add_skill("datasphere", { - {"document_id", "drinks-doc-123"}, - {"tool_name", "search_drinks_knowledge"}, - {"count", 2}, - {"distance", 5.0} - }); + agent.add_skill("datasphere", {{"document_id", "drinks-doc-123"}, + {"tool_name", "search_drinks_knowledge"}, + {"count", 2}, + {"distance", 5.0}}); // Instance 2: Food knowledge - agent.add_skill("datasphere", { - {"document_id", "food-doc-456"}, - {"tool_name", "search_food_knowledge"}, - {"count", 3}, - {"distance", 4.0} - }); + agent.add_skill("datasphere", {{"document_id", "food-doc-456"}, + {"tool_name", "search_food_knowledge"}, + {"count", 3}, + {"distance", 4.0}}); // Instance 3: General knowledge (default tool name) - agent.add_skill("datasphere", { - {"document_id", "general-doc-789"}, - {"count", 1}, - {"distance", 3.0} - }); + agent.add_skill("datasphere", + {{"document_id", "general-doc-789"}, {"count", 1}, {"distance", 3.0}}); std::cout << "Multi-DataSphere agent at http://0.0.0.0:3000/datasphere-multi\n"; std::cout << "Tools: search_drinks_knowledge, search_food_knowledge, search_knowledge\n"; std::cout << "Note: Replace document IDs with your actual DataSphere documents.\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/datasphere_serverless_env.cpp b/examples/datasphere_serverless_env.cpp index 0782119..7703aa8 100644 --- a/examples/datasphere_serverless_env.cpp +++ b/examples/datasphere_serverless_env.cpp @@ -3,35 +3,56 @@ // Required: DATASPHERE_DOCUMENT_ID // Optional: DATASPHERE_COUNT, DATASPHERE_DISTANCE, DATASPHERE_TAGS -#include #include +#include +#include +#include +#include using namespace signalwire; using json = nlohmann::json; std::string require_env(const char* name) { - const char* val = std::getenv(name); - if (!val || std::string(val).empty()) { - std::cerr << "Error: Required environment variable " << name << " is not set.\n"; - std::exit(1); - } - return val; + const char* val = std::getenv(name); + if (!val || std::string(val).empty()) { + std::cerr << "Error: Required environment variable " << name << " is not set.\n"; + std::exit(1); + } + return val; } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { std::string document_id = require_env("DATASPHERE_DOCUMENT_ID"); int count = 3; - if (auto v = std::getenv("DATASPHERE_COUNT")) count = std::atoi(v); + if (auto v = std::getenv("DATASPHERE_COUNT")) { + // atoi() cannot report a bad value -- it returns 0, which would + // silently ask for zero results. Parse it properly. + try { + count = std::stoi(v); + } catch (const std::exception&) { + std::cerr << "DATASPHERE_COUNT=\"" << v << "\" is not a number; using " << count << "\n"; + } + } double distance = 4.0; - if (auto v = std::getenv("DATASPHERE_DISTANCE")) distance = std::atof(v); + if (auto v = std::getenv("DATASPHERE_DISTANCE")) { + try { + distance = std::stod(v); + } catch (const std::exception&) { + std::cerr << "DATASPHERE_DISTANCE=\"" << v << "\" is not a number; using " << distance + << "\n"; + } + } agent::AgentBase agent("datasphere-serverless-env", "/datasphere-env"); agent.prompt_add_section("Role", - "You are a knowledge assistant with access to a document library via " - "serverless DataSphere."); + "You are a knowledge assistant with access to a document library via " + "serverless DataSphere."); agent.add_language({"English", "en-US", "inworld.Mark"}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); @@ -39,23 +60,21 @@ int main() { agent.add_skill("datetime", {}); agent.add_skill("math", {}); - json config = { - {"document_id", document_id}, - {"count", count}, - {"distance", distance} - }; + json config = {{"document_id", document_id}, {"count", count}, {"distance", distance}}; if (auto tags = std::getenv("DATASPHERE_TAGS")) { - // Simple comma-split - json tag_array = json::array(); - std::string t(tags); - size_t pos = 0; - while ((pos = t.find(',')) != std::string::npos) { - tag_array.push_back(t.substr(0, pos)); - t.erase(0, pos + 1); - } - if (!t.empty()) tag_array.push_back(t); - config["tags"] = tag_array; + // Simple comma-split + json tag_array = json::array(); + std::string t(tags); + size_t pos = 0; + while ((pos = t.find(',')) != std::string::npos) { + tag_array.push_back(t.substr(0, pos)); + t.erase(0, pos + 1); + } + if (!t.empty()) { + tag_array.push_back(t); + } + config["tags"] = tag_array; } agent.add_skill("datasphere", config); @@ -64,4 +83,8 @@ int main() { std::cout << " Document: " << document_id << "\n"; std::cout << " Count: " << count << ", Distance: " << distance << "\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/datasphere_webhook_env_demo.cpp b/examples/datasphere_webhook_env_demo.cpp index 4042ce0..fdc1671 100644 --- a/examples/datasphere_webhook_env_demo.cpp +++ b/examples/datasphere_webhook_env_demo.cpp @@ -3,33 +3,54 @@ // Compare with datasphere_serverless_env.cpp for the serverless approach. // Required: DATASPHERE_DOCUMENT_ID -#include #include +#include +#include +#include +#include using namespace signalwire; std::string require_env(const char* name) { - const char* val = std::getenv(name); - if (!val || std::string(val).empty()) { - std::cerr << "Error: Required environment variable " << name << " is not set.\n"; - std::exit(1); - } - return val; + const char* val = std::getenv(name); + if (!val || std::string(val).empty()) { + std::cerr << "Error: Required environment variable " << name << " is not set.\n"; + std::exit(1); + } + return val; } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { std::string document_id = require_env("DATASPHERE_DOCUMENT_ID"); int count = 3; - if (auto v = std::getenv("DATASPHERE_COUNT")) count = std::atoi(v); + if (auto v = std::getenv("DATASPHERE_COUNT")) { + // atoi() cannot report a bad value -- it returns 0, which would + // silently ask for zero results. Parse it properly. + try { + count = std::stoi(v); + } catch (const std::exception&) { + std::cerr << "DATASPHERE_COUNT=\"" << v << "\" is not a number; using " << count << "\n"; + } + } double distance = 4.0; - if (auto v = std::getenv("DATASPHERE_DISTANCE")) distance = std::atof(v); + if (auto v = std::getenv("DATASPHERE_DISTANCE")) { + try { + distance = std::stod(v); + } catch (const std::exception&) { + std::cerr << "DATASPHERE_DISTANCE=\"" << v << "\" is not a number; using " << distance + << "\n"; + } + } agent::AgentBase agent("datasphere-webhook-env", "/datasphere-webhook"); - agent.prompt_add_section("Role", - "You are a knowledge assistant using webhook-based DataSphere for retrieval."); + agent.prompt_add_section( + "Role", "You are a knowledge assistant using webhook-based DataSphere for retrieval."); agent.add_language({"English", "en-US", "inworld.Mark"}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); @@ -37,12 +58,10 @@ int main() { agent.add_skill("datetime", {}); agent.add_skill("math", {}); - agent.add_skill("datasphere", { - {"document_id", document_id}, - {"count", count}, - {"distance", distance}, - {"mode", "webhook"} - }); + agent.add_skill("datasphere", {{"document_id", document_id}, + {"count", count}, + {"distance", distance}, + {"mode", "webhook"}}); std::cout << "DataSphere Webhook Environment Demo\n"; std::cout << " Document: " << document_id << "\n"; @@ -50,4 +69,8 @@ int main() { std::cout << " Webhook: Full control, custom error handling\n"; std::cout << " Serverless: No webhooks, lower latency, executes on SignalWire\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/declarative_agent.cpp b/examples/declarative_agent.cpp index cda8fb7..475f4dc 100644 --- a/examples/declarative_agent.cpp +++ b/examples/declarative_agent.cpp @@ -1,30 +1,32 @@ // Copyright (c) 2025 SignalWire — MIT License // Declarative agent: build entirely from JSON-like config. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("declarative", "/declarative"); // Configure everything declaratively agent.set_prompt_text("You are a helpful assistant for Acme Corp."); agent.set_post_prompt("Summarize the conversation as JSON."); - agent.set_params({ - {"ai_model", "gpt-4.1-nano"}, - {"wait_for_user", false}, - {"end_of_speech_timeout", 1000} - }); - agent.set_global_data({ - {"company", "Acme Corp"}, - {"department", "Sales"} - }); + agent.set_params( + {{"ai_model", "gpt-4.1-nano"}, {"wait_for_user", false}, {"end_of_speech_timeout", 1000}}); + agent.set_global_data({{"company", "Acme Corp"}, {"department", "Sales"}}); agent.add_hints({"Acme", "SWML"}); agent.set_native_functions({"check_time"}); agent.add_language({"English", "en-US", "inworld.Mark"}); std::cout << "Declarative agent at http://0.0.0.0:3000/declarative\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/dynamic_info_gatherer_example.cpp b/examples/dynamic_info_gatherer_example.cpp index 246a9f5..3141ce6 100644 --- a/examples/dynamic_info_gatherer_example.cpp +++ b/examples/dynamic_info_gatherer_example.cpp @@ -3,57 +3,65 @@ // at configuration time. The C++ port exposes a static set_questions() // surface; per-request dynamic selection is not yet implemented in C++. +#include #include -#include #include -#include +#include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { // Define question sets std::map question_sets; - question_sets["default"] = json::array({ - {{"key_name", "name"}, {"question_text", "What is your full name?"}}, - {{"key_name", "phone"}, {"question_text", "What is your phone number?"}, {"confirm", true}}, - {{"key_name", "reason"}, {"question_text", "How can I help you today?"}} - }); + question_sets["default"] = + json::array({{{"key_name", "name"}, {"question_text", "What is your full name?"}}, + {{"key_name", "phone"}, + {"question_text", "What is your phone number?"}, + {"confirm", true}}, + {{"key_name", "reason"}, {"question_text", "How can I help you today?"}}}); - question_sets["support"] = json::array({ - {{"key_name", "customer_name"}, {"question_text", "What is your name?"}}, - {{"key_name", "account_number"}, {"question_text", "What is your account number?"}, {"confirm", true}}, - {{"key_name", "issue"}, {"question_text", "What issue are you experiencing?"}}, - {{"key_name", "priority"}, {"question_text", "How urgent is this? (Low, Medium, High)"}} - }); + question_sets["support"] = json::array( + {{{"key_name", "customer_name"}, {"question_text", "What is your name?"}}, + {{"key_name", "account_number"}, + {"question_text", "What is your account number?"}, + {"confirm", true}}, + {{"key_name", "issue"}, {"question_text", "What issue are you experiencing?"}}, + {{"key_name", "priority"}, {"question_text", "How urgent is this? (Low, Medium, High)"}}}); - question_sets["medical"] = json::array({ - {{"key_name", "patient_name"}, {"question_text", "What is the patient's full name?"}}, - {{"key_name", "symptoms"}, {"question_text", "What symptoms are you experiencing?"}, {"confirm", true}}, - {{"key_name", "duration"}, {"question_text", "How long have you had these symptoms?"}}, - {{"key_name", "medications"}, {"question_text", "Are you currently taking any medications?"}} - }); + question_sets["medical"] = json::array( + {{{"key_name", "patient_name"}, {"question_text", "What is the patient's full name?"}}, + {{"key_name", "symptoms"}, + {"question_text", "What symptoms are you experiencing?"}, + {"confirm", true}}, + {{"key_name", "duration"}, {"question_text", "How long have you had these symptoms?"}}, + {{"key_name", "medications"}, + {"question_text", "Are you currently taking any medications?"}}}); - question_sets["onboarding"] = json::array({ - {{"key_name", "full_name"}, {"question_text", "What is your full name?"}}, - {{"key_name", "email"}, {"question_text", "What is your email address?"}, {"confirm", true}}, - {{"key_name", "company"}, {"question_text", "What company do you work for?"}}, - {{"key_name", "department"}, {"question_text", "What department?"}}, - {{"key_name", "start_date"}, {"question_text", "What is your start date?"}} - }); + question_sets["onboarding"] = + json::array({{{"key_name", "full_name"}, {"question_text", "What is your full name?"}}, + {{"key_name", "email"}, + {"question_text", "What is your email address?"}, + {"confirm", true}}, + {{"key_name", "company"}, {"question_text", "What company do you work for?"}}, + {{"key_name", "department"}, {"question_text", "What department?"}}, + {{"key_name", "start_date"}, {"question_text", "What is your start date?"}}}); // Choose the question set at startup via the QUESTION_SET env var. std::string set_name = signalwire::get_env("QUESTION_SET", "default"); auto selected = question_sets.find(set_name); if (selected == question_sets.end()) { - std::cout << "Unknown QUESTION_SET=" << set_name << "; using default.\n"; - selected = question_sets.find("default"); + std::cout << "Unknown QUESTION_SET=" << set_name << "; using default.\n"; + selected = question_sets.find("default"); } std::vector questions; for (const auto& q : selected->second) { - questions.push_back(q); + questions.push_back(q); } prefabs::InfoGathererAgent gatherer("dynamic-intake", "/contact"); @@ -62,4 +70,8 @@ int main() { std::cout << "InfoGatherer (" << set_name << ") at http://0.0.0.0:3000/contact\n"; std::cout << "Select a set at startup with QUESTION_SET={default|support|medical|onboarding}\n"; gatherer.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/dynamic_swml_service.cpp b/examples/dynamic_swml_service.cpp index 810e57a..f7f7906 100644 --- a/examples/dynamic_swml_service.cpp +++ b/examples/dynamic_swml_service.cpp @@ -1,27 +1,34 @@ // Copyright (c) 2025 SignalWire — MIT License // Dynamic SWML service: different documents per request. -#include #include +#include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { swml::Service svc; svc.set_route("/dynamic-swml"); svc.set_port(3000); // Base document svc.answer(); - svc.ai({ - {"prompt", {{"text", "You are a helpful assistant. " - "Customize via query params: ?persona=sales"}}}, - {"post_prompt", {{"text", "Summarize in JSON."}}} - }); + svc.ai({{"prompt", + {{"text", + "You are a helpful assistant. " + "Customize via query params: ?persona=sales"}}}, + {"post_prompt", {{"text", "Summarize in JSON."}}}}); svc.hangup(); std::cout << "Base SWML:\n" << svc.render_swml().dump(2) << "\n\n"; std::cout << "Dynamic SWML Service at http://0.0.0.0:3000/dynamic-swml\n"; svc.serve(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/faq_bot_agent.cpp b/examples/faq_bot_agent.cpp index 883e5e4..e365b7b 100644 --- a/examples/faq_bot_agent.cpp +++ b/examples/faq_bot_agent.cpp @@ -1,23 +1,34 @@ // Copyright (c) 2025 SignalWire — MIT License // FAQ Bot prefab: keyword-based FAQ matching. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { prefabs::FAQBotAgent agent("faq", "/faq"); - agent.set_faqs({ - {{"question", "What are your hours?"}, {"answer", "We are open Monday-Friday 9AM-5PM EST."}}, - {{"question", "What is your return policy?"}, {"answer", "You can return items within 30 days for a full refund."}}, - {{"question", "How do I contact support?"}, {"answer", "Email support@example.com or call 1-800-EXAMPLE."}}, - {{"question", "Do you ship internationally?"}, {"answer", "Yes, we ship to over 50 countries."}} - }); - agent.set_no_match_message("I don't have an answer for that. Let me connect you with a human agent."); + agent.set_faqs({{{"question", "What are your hours?"}, + {"answer", "We are open Monday-Friday 9AM-5PM EST."}}, + {{"question", "What is your return policy?"}, + {"answer", "You can return items within 30 days for a full refund."}}, + {{"question", "How do I contact support?"}, + {"answer", "Email support@example.com or call 1-800-EXAMPLE."}}, + {{"question", "Do you ship internationally?"}, + {"answer", "Yes, we ship to over 50 countries."}}}); + agent.set_no_match_message( + "I don't have an answer for that. Let me connect you with a human agent."); agent.set_suggest_related(true); std::cout << "FAQ Bot at http://0.0.0.0:3000/faq\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/gather_info_demo.cpp b/examples/gather_info_demo.cpp index 3ee1dc4..a2c677a 100644 --- a/examples/gather_info_demo.cpp +++ b/examples/gather_info_demo.cpp @@ -1,11 +1,15 @@ // Copyright (c) 2025 SignalWire — MIT License // GatherInfo demo using contexts/steps with question collection. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("gather-info", "/gather-info"); agent.prompt_add_section("Role", "You are a friendly intake assistant."); @@ -31,4 +35,8 @@ int main() { std::cout << "GatherInfo demo at http://0.0.0.0:3000/gather-info\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/gather_per_question_functions_demo.cpp b/examples/gather_per_question_functions_demo.cpp index b51940b..8f03972 100644 --- a/examples/gather_per_question_functions_demo.cpp +++ b/examples/gather_per_question_functions_demo.cpp @@ -27,61 +27,53 @@ // // Run this file to see the resulting SWML. +#include #include #include -#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("gather_per_question_functions_demo", "/"); // Tools that the step would normally have available — but during // gather questioning, they're all locked out unless they appear // in a question's `functions` whitelist. agent.define_tool( - "validate_email", - "Validate that an email address is well-formed and deliverable", - {{"email", {{"type", "string"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("valid"); + "validate_email", "Validate that an email address is well-formed and deliverable", + {{"email", {{"type", "string"}}}}, [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("valid"); }); - agent.define_tool( - "geocode_zip", - "Look up the city/state for a US ZIP code", - {{"zip", {{"type", "string"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult(R"({"city":"...","state":"..."})"); - }); + agent.define_tool("geocode_zip", "Look up the city/state for a US ZIP code", + {{"zip", {{"type", "string"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult(R"({"city":"...","state":"..."})"); + }); - agent.define_tool( - "check_age_eligibility", - "Verify the customer is old enough for the product", - {{"age", {{"type", "integer"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("eligible"); - }); + agent.define_tool("check_age_eligibility", "Verify the customer is old enough for the product", + {{"age", {{"type", "integer"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("eligible"); + }); // These tools are NOT whitelisted on any gather question. They // are registered on the agent and active outside the gather, but // during the gather they cannot be called — gather mode locks // them out. - agent.define_tool( - "escalate_to_human", - "Transfer the conversation to a live agent", - nlohmann::json::object(), - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("transferred"); - }); + agent.define_tool("escalate_to_human", "Transfer the conversation to a live agent", + nlohmann::json::object(), [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("transferred"); + }); - agent.define_tool( - "lookup_existing_account", - "Search for an existing account by email", - {{"email", {{"type", "string"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("not found"); - }); + agent.define_tool("lookup_existing_account", "Search for an existing account by email", + {{"email", {{"type", "string"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("not found"); + }); // Build a single-context agent with one onboarding step. auto& cb = agent.define_contexts(); @@ -102,43 +94,37 @@ int main() { "escalate_to_human", "lookup_existing_account", }) - .set_gather_info( - "customer", - "next_step", - "I'll need to collect a few details to set up your " - "account. I'll ask one question at a time."); + .set_gather_info("customer", "next_step", + "I'll need to collect a few details to set up your " + "account. I'll ask one question at a time."); // Question 1: email — only validate_email + gather_submit callable. - onboard.add_gather_question( - "email", "What's your email address?", - /*type*/ "string", - /*confirm*/ true, - /*prompt*/ "", - /*functions*/ {"validate_email"}); + onboard.add_gather_question("email", "What's your email address?", + /*type*/ "string", + /*confirm*/ true, + /*prompt*/ "", + /*functions*/ {"validate_email"}); // Question 2: zip — only geocode_zip + gather_submit callable. - onboard.add_gather_question( - "zip", "What's your ZIP code?", - /*type*/ "string", - /*confirm*/ false, - /*prompt*/ "", - /*functions*/ {"geocode_zip"}); + onboard.add_gather_question("zip", "What's your ZIP code?", + /*type*/ "string", + /*confirm*/ false, + /*prompt*/ "", + /*functions*/ {"geocode_zip"}); // Question 3: age — only check_age_eligibility + gather_submit // callable. - onboard.add_gather_question( - "age", "How old are you?", - /*type*/ "integer", - /*confirm*/ false, - /*prompt*/ "", - /*functions*/ {"check_age_eligibility"}); + onboard.add_gather_question("age", "How old are you?", + /*type*/ "integer", + /*confirm*/ false, + /*prompt*/ "", + /*functions*/ {"check_age_eligibility"}); // Question 4: referral_source — no functions → only gather_submit // is callable. The model cannot validate, lookup, escalate — // nothing. This is the right pattern when a question needs no // tools. - onboard.add_gather_question( - "referral_source", "How did you hear about us?"); + onboard.add_gather_question("referral_source", "How did you hear about us?"); // A simple confirmation step the gather auto-advances into. ctx.add_step("confirm") @@ -149,6 +135,10 @@ int main() { .set_end(true); auto swml = agent.render_swml(); - std::cout << swml.dump(2) << std::endl; + std::cout << swml.dump(2) << '\n'; return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/info_gatherer_example.cpp b/examples/info_gatherer_example.cpp index 652c8e3..88f040c 100644 --- a/examples/info_gatherer_example.cpp +++ b/examples/info_gatherer_example.cpp @@ -1,24 +1,30 @@ // Copyright (c) 2025 SignalWire — MIT License // InfoGatherer prefab: sequential question collection. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { prefabs::InfoGathererAgent agent("contact-form", "/contact-form"); - agent.set_questions({ - {{"key", "name"}, {"question", "What is your full name?"}}, - {{"key", "email"}, {"question", "What is your email address?"}}, - {{"key", "phone"}, {"question", "What is your phone number?"}}, - {{"key", "reason"}, {"question", "How can we help you today?"}} - }); + agent.set_questions({{{"key", "name"}, {"question", "What is your full name?"}}, + {{"key", "email"}, {"question", "What is your email address?"}}, + {{"key", "phone"}, {"question", "What is your phone number?"}}, + {{"key", "reason"}, {"question", "How can we help you today?"}}}); agent.set_completion_message("Thank you! We have your information and will follow up."); agent.set_prefix("contact"); std::cout << "InfoGatherer at http://0.0.0.0:3000/contact-form\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/joke_agent.cpp b/examples/joke_agent.cpp index c1666d3..be4822a 100644 --- a/examples/joke_agent.cpp +++ b/examples/joke_agent.cpp @@ -1,19 +1,21 @@ // Copyright (c) 2025 SignalWire — MIT License // Joke agent: uses the joke skill for entertainment. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("joke-teller", "/jokes"); agent.prompt_add_section("Personality", "You are a hilarious comedian."); - agent.prompt_add_section("Instructions", "", { - "Tell jokes when asked", - "Keep the humor clean and family-friendly", - "Use different joke categories to keep things fresh" - }); + agent.prompt_add_section("Instructions", "", + {"Tell jokes when asked", "Keep the humor clean and family-friendly", + "Use different joke categories to keep things fresh"}); agent.add_skill("joke"); agent.add_skill("datetime"); @@ -21,4 +23,8 @@ int main() { std::cout << "Joke agent at http://0.0.0.0:3000/jokes\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/joke_skill_demo.cpp b/examples/joke_skill_demo.cpp index 8b11eb4..da5c477 100644 --- a/examples/joke_skill_demo.cpp +++ b/examples/joke_skill_demo.cpp @@ -3,28 +3,30 @@ // Compare with joke_agent.cpp (raw data_map). // Required: API_NINJAS_KEY environment variable. -#include #include +#include +#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { const char* api_key = std::getenv("API_NINJAS_KEY"); if (!api_key || std::string(api_key).empty()) { - std::cerr << "Error: API_NINJAS_KEY environment variable is required.\n"; - std::cerr << "Get your free API key from https://api.api-ninjas.com/\n"; - return 1; + std::cerr << "Error: API_NINJAS_KEY environment variable is required.\n"; + std::cerr << "Get your free API key from https://api.api-ninjas.com/\n"; + return 1; } agent::AgentBase agent("joke-skill-demo", "/joke-skill"); - agent.prompt_add_section("Personality", - "You are a cheerful comedian who loves sharing jokes."); - agent.prompt_add_section("Instructions", "", { - "When users ask for jokes, use your joke functions", - "Be enthusiastic and fun in your responses", - "You can tell both regular jokes and dad jokes" - }); + agent.prompt_add_section("Personality", "You are a cheerful comedian who loves sharing jokes."); + agent.prompt_add_section("Instructions", "", + {"When users ask for jokes, use your joke functions", + "Be enthusiastic and fun in your responses", + "You can tell both regular jokes and dad jokes"}); agent.add_language({"English", "en-US", "inworld.Mark"}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); @@ -37,4 +39,8 @@ int main() { std::cout << " - Automatic validation and error handling\n"; std::cout << " - Reusable across agents\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/kubernetes_ready_agent.cpp b/examples/kubernetes_ready_agent.cpp index c84d04b..544be8d 100644 --- a/examples/kubernetes_ready_agent.cpp +++ b/examples/kubernetes_ready_agent.cpp @@ -1,8 +1,11 @@ // Copyright (c) 2025 SignalWire — MIT License // Kubernetes-ready agent with health checks and graceful shutdown. -#include #include +#include +#include +#include +#include using namespace signalwire; using json = nlohmann::json; @@ -10,15 +13,28 @@ using json = nlohmann::json; static agent::AgentBase* g_agent = nullptr; void signal_handler(int sig) { - (void)sig; - if (g_agent) g_agent->stop(); + (void)sig; + if (g_agent) { + g_agent->stop(); + } } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { // Read port from env (Kubernetes can set this) int port = 3000; const char* port_env = std::getenv("PORT"); - if (port_env) port = std::atoi(port_env); + if (port_env) { + // atoi() reports NOTHING on a non-numeric value -- it just returns 0, + // so PORT=abc used to bind port 0. Parse it properly and say so. + try { + port = std::stoi(port_env); + } catch (const std::exception&) { + std::cerr << "PORT=\"" << port_env << "\" is not a number; using " << port << "\n"; + } + } agent::AgentBase agent("k8s-agent", "/", "0.0.0.0", port); g_agent = &agent; @@ -29,11 +45,12 @@ int main() { // Health check tool agent.define_tool("health_check", "Check agent health", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Agent is healthy and running."); - }); + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + return swaig::FunctionResult("Agent is healthy and running."); + }); // Graceful shutdown std::signal(SIGTERM, signal_handler); @@ -41,4 +58,8 @@ int main() { std::cout << "Kubernetes-ready agent at http://0.0.0.0:" << port << "/\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/lambda_agent.cpp b/examples/lambda_agent.cpp index 83d1fd8..cc2409c 100644 --- a/examples/lambda_agent.cpp +++ b/examples/lambda_agent.cpp @@ -4,54 +4,62 @@ // with a Lambda adapter (API Gateway proxy integration). // For local testing, runs as a normal HTTP server. -#include #include +#include +#include using namespace signalwire; using json = nlohmann::json; // Global agent created once per Lambda cold start static agent::AgentBase& get_agent() { - static agent::AgentBase a("lambda-agent", "/"); - - a.add_language({"English", "en-US", "inworld.Mark"}); - - a.prompt_add_section("Role", - "You are a helpful AI assistant running in a serverless environment."); - a.prompt_add_section("Instructions", "", { - "Greet users warmly and offer help", - "Use the greet_user function when asked to greet someone", - "Use the get_time function when asked about the current time" - }); - - a.define_tool("greet_user", "Greet a user by name", - {{"type", "object"}, {"properties", { - {"name", {{"type", "string"}, {"description", "Name of the user"}}} - }}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string name = args.value("name", "friend"); - return swaig::FunctionResult("Hello " + name + "! I'm running in serverless mode!"); - }); - - a.define_tool("get_time", "Get the current time", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - auto t = std::time(nullptr); - char buf[32]; - std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::localtime(&t)); - return swaig::FunctionResult(std::string("Current time: ") + buf); - }); - - return a; + static agent::AgentBase a("lambda-agent", "/"); + + a.add_language({"English", "en-US", "inworld.Mark"}); + + a.prompt_add_section("Role", + "You are a helpful AI assistant running in a serverless environment."); + a.prompt_add_section("Instructions", "", + {"Greet users warmly and offer help", + "Use the greet_user function when asked to greet someone", + "Use the get_time function when asked about the current time"}); + + a.define_tool( + "greet_user", "Greet a user by name", + {{"type", "object"}, + {"properties", {{"name", {{"type", "string"}, {"description", "Name of the user"}}}}}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)raw; + std::string name = args.value("name", "friend"); + return swaig::FunctionResult("Hello " + name + "! I'm running in serverless mode!"); + }); + + a.define_tool("get_time", "Get the current time", + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + auto t = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::localtime(&t)); + return swaig::FunctionResult(std::string("Current time: ") + buf); + }); + + return a; } // In production: wrap get_agent() HTTP endpoints with a Lambda adapter. // For local testing: int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { auto& agent = get_agent(); std::cout << "Starting Lambda agent (local testing) at http://0.0.0.0:3000/\n"; std::cout << "In production, wrap render_swml() / HTTP handlers with a Lambda adapter.\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/llm_params_demo.cpp b/examples/llm_params_demo.cpp index 87b4172..d6ff8a9 100644 --- a/examples/llm_params_demo.cpp +++ b/examples/llm_params_demo.cpp @@ -1,39 +1,40 @@ // Copyright (c) 2025 SignalWire — MIT License // LLM parameter tuning for different use cases. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("llm-params", "/llm-params"); agent.prompt_add_section("Role", "You are a technical support agent."); // Conservative parameters for technical support - agent.set_prompt_llm_params({ - {"temperature", 0.2}, - {"top_p", 0.9}, - {"barge_confidence", 0.8}, - {"presence_penalty", 0.0}, - {"frequency_penalty", 0.1} - }); + agent.set_prompt_llm_params({{"temperature", 0.2}, + {"top_p", 0.9}, + {"barge_confidence", 0.8}, + {"presence_penalty", 0.0}, + {"frequency_penalty", 0.1}}); // Different parameters for post-prompt analysis - agent.set_post_prompt_llm_params({ - {"temperature", 0.1}, - {"top_p", 0.95} - }); + agent.set_post_prompt_llm_params({{"temperature", 0.1}, {"top_p", 0.95}}); - agent.set_post_prompt("Classify the issue: {\"category\": \"...\", \"severity\": \"low|medium|high\"}"); + agent.set_post_prompt( + "Classify the issue: {\"category\": \"...\", \"severity\": \"low|medium|high\"}"); - agent.set_params({ - {"ai_model", "gpt-4.1-nano"}, - {"end_of_speech_timeout", 1500}, - {"ai_volume", 5} - }); + agent.set_params( + {{"ai_model", "gpt-4.1-nano"}, {"end_of_speech_timeout", 1500}, {"ai_volume", 5}}); std::cout << "LLM params demo at http://0.0.0.0:3000/llm-params\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/mcp_agent.cpp b/examples/mcp_agent.cpp index 26ec9aa..4ba631e 100644 --- a/examples/mcp_agent.cpp +++ b/examples/mcp_agent.cpp @@ -12,66 +12,74 @@ // Build: cmake --build build // Run: ./build/examples/mcp_agent +#include #include using namespace signalwire; using json = nlohmann::json; class McpAgent : public agent::AgentBase { -public: - McpAgent() : AgentBase("mcp-agent", "/agent") { - // -- MCP Server -- - // Adds a /mcp endpoint that speaks JSON-RPC 2.0 (MCP protocol). - enable_mcp_server(); + public: + McpAgent() : AgentBase("mcp-agent", "/agent") { + // -- MCP Server -- + // Adds a /mcp endpoint that speaks JSON-RPC 2.0 (MCP protocol). + enable_mcp_server(); - // -- MCP Client -- - // Connect to an external MCP server. Tools are discovered automatically. - add_mcp_server("https://mcp.example.com/tools", - {{"Authorization", "Bearer sk-your-mcp-api-key"}}); + // -- MCP Client -- + // Connect to an external MCP server. Tools are discovered automatically. + add_mcp_server("https://mcp.example.com/tools", + {{"Authorization", "Bearer sk-your-mcp-api-key"}}); - // -- MCP Client with Resources -- - add_mcp_server("https://mcp.example.com/crm", - {{"Authorization", "Bearer sk-your-crm-key"}}, - true, - {{"caller_id", "${caller_id_number}"}, {"tenant", "acme-corp"}}); + // -- MCP Client with Resources -- + add_mcp_server("https://mcp.example.com/crm", {{"Authorization", "Bearer sk-your-crm-key"}}, + true, {{"caller_id", "${caller_id_number}"}, {"tenant", "acme-corp"}}); - // -- Agent Configuration -- - prompt_add_section("Role", - "You are a helpful customer support agent. " - "You have access to the customer's profile via global_data."); + // -- Agent Configuration -- + prompt_add_section("Role", + "You are a helpful customer support agent. " + "You have access to the customer's profile via global_data."); - set_params({{"attention_timeout", 15000}}); + set_params({{"attention_timeout", 15000}}); - // -- Local Tools -- - define_tool("get_weather", "Get the current weather for a location", - json::object({ - {"type", "object"}, - {"properties", json::object({ - {"location", json::object({{"type", "string"}, {"description", "City name or zip code"}})} - })} - }), - [](const json& args, const json&) -> swaig::FunctionResult { - std::string location = args.value("location", "unknown"); - return swaig::FunctionResult("Currently 72F and sunny in " + location + "."); - }); + // -- Local Tools -- + define_tool("get_weather", "Get the current weather for a location", + json::object( + {{"type", "object"}, + {"properties", + json::object({{"location", + json::object({{"type", "string"}, + {"description", "City name or zip code"}})}})}}), + [](const json& args, const json&) -> swaig::FunctionResult { + std::string location = args.value("location", "unknown"); + return swaig::FunctionResult("Currently 72F and sunny in " + location + "."); + }); - define_tool("create_ticket", "Create a support ticket", - json::object({ - {"type", "object"}, - {"properties", json::object({ - {"subject", json::object({{"type", "string"}, {"description", "Ticket subject"}})}, - {"description", json::object({{"type", "string"}, {"description", "Issue description"}})} - })} - }), - [](const json& args, const json&) -> swaig::FunctionResult { - std::string subject = args.value("subject", "No subject"); - return swaig::FunctionResult("Ticket created: '" + subject + "'. Reference: TK-12345."); - }); - } + define_tool( + "create_ticket", "Create a support ticket", + json::object( + {{"type", "object"}, + {"properties", + json::object( + {{"subject", + json::object({{"type", "string"}, {"description", "Ticket subject"}})}, + {"description", + json::object({{"type", "string"}, {"description", "Issue description"}})}})}}), + [](const json& args, const json&) -> swaig::FunctionResult { + std::string subject = args.value("subject", "No subject"); + return swaig::FunctionResult("Ticket created: '" + subject + "'. Reference: TK-12345."); + }); + } }; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { McpAgent agent; agent.run(); return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/mcp_gateway_demo.cpp b/examples/mcp_gateway_demo.cpp index 0f49612..54329fd 100644 --- a/examples/mcp_gateway_demo.cpp +++ b/examples/mcp_gateway_demo.cpp @@ -1,29 +1,33 @@ // Copyright (c) 2025 SignalWire — MIT License // MCP gateway demo: agent with MCP skill integration. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("mcp-gateway", "/mcp-gateway"); agent.prompt_add_section("Role", "You are an assistant with MCP tool access."); - agent.prompt_add_section("Instructions", "", { - "Use MCP tools when the user requests external operations", - "Explain what tools are available if asked" - }); + agent.prompt_add_section("Instructions", "", + {"Use MCP tools when the user requests external operations", + "Explain what tools are available if asked"}); // MCP gateway skill - agent.add_skill("mcp_gateway", { - {"url", "http://localhost:8080/mcp"}, - {"timeout", 30} - }); + agent.add_skill("mcp_gateway", {{"url", "http://localhost:8080/mcp"}, {"timeout", 30}}); agent.add_skill("datetime"); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); std::cout << "MCP Gateway at http://0.0.0.0:3000/mcp-gateway\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/multi_agent_server.cpp b/examples/multi_agent_server.cpp index cb28168..1994b0c 100644 --- a/examples/multi_agent_server.cpp +++ b/examples/multi_agent_server.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2025 SignalWire — MIT License // Multi-agent server hosting several agents on one port. +#include #include #include @@ -8,6 +9,9 @@ using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { // Create agents auto sales = std::make_shared("sales", "/sales"); sales->prompt_add_section("Role", "You are a sales representative."); @@ -31,4 +35,8 @@ int main() { std::cout << "Multi-agent server running on http://0.0.0.0:3000\n"; std::cout << " /sales, /support, /billing\n"; srv.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/multi_endpoint_agent.cpp b/examples/multi_endpoint_agent.cpp index 5948df6..8872f73 100644 --- a/examples/multi_endpoint_agent.cpp +++ b/examples/multi_endpoint_agent.cpp @@ -1,12 +1,16 @@ // Copyright (c) 2025 SignalWire — MIT License // Multi-endpoint agent with multiple webhook URLs and query params. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("multi-endpoint", "/multi-endpoint"); agent.prompt_add_section("Role", "You are a multi-endpoint demo agent."); @@ -20,20 +24,22 @@ int main() { agent.add_swaig_query_param("env", "production"); // Function includes from remote servers - agent.add_function_include({ - {"url", "https://tools.example.com/functions"}, - {"functions", {"translate", "summarize"}} - }); - - agent.define_tool("local_tool", "A locally-handled tool", - {{"type", "object"}, {"properties", { - {"input", {{"type", "string"}, {"description", "Input text"}}} - }}}, + agent.add_function_include({{"url", "https://tools.example.com/functions"}, + {"functions", {"translate", "summarize"}}}); + + agent.define_tool( + "local_tool", "A locally-handled tool", + {{"type", "object"}, + {"properties", {{"input", {{"type", "string"}, {"description", "Input text"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - return swaig::FunctionResult("Processed: " + args.value("input", "")); + (void)raw; + return swaig::FunctionResult("Processed: " + args.value("input", "")); }); std::cout << "Multi-endpoint at http://0.0.0.0:3000/multi-endpoint\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/quickstart_agent.cpp b/examples/quickstart_agent.cpp index 14f23bc..e34d91f 100644 --- a/examples/quickstart_agent.cpp +++ b/examples/quickstart_agent.cpp @@ -6,31 +6,39 @@ // README-INCLUDE gate, so the doc code can never drift from working code. // region: agent -#include #include +#include +#include using namespace signalwire; using json = nlohmann::json; class MyAgent : public agent::AgentBase { -public: - MyAgent() : AgentBase("my-agent", "/agent") { - add_language({"English", "en-US", "inworld.Mark"}); - prompt_add_section("Role", "You are a helpful assistant."); + public: + MyAgent() : AgentBase("my-agent", "/agent") { + add_language({"English", "en-US", "inworld.Mark"}); + prompt_add_section("Role", "You are a helpful assistant."); - define_tool("get_time", "Get the current time", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& /*args*/, const json& /*raw*/) -> swaig::FunctionResult { - auto now = std::time(nullptr); - char buf[32]; - std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&now)); - return swaig::FunctionResult(std::string("The time is ") + buf); - }); - } + define_tool("get_time", "Get the current time", + {{"type", "object"}, {"properties", json::object()}}, + [](const json& /*args*/, const json& /*raw*/) -> swaig::FunctionResult { + auto now = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&now)); + return swaig::FunctionResult(std::string("The time is ") + buf); + }); + } }; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { MyAgent agent; agent.run(); // Serves on http://0.0.0.0:3000/agent + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } // endregion: agent diff --git a/examples/quickstart_relay.cpp b/examples/quickstart_relay.cpp index 0f0f40c..efa9038 100644 --- a/examples/quickstart_relay.cpp +++ b/examples/quickstart_relay.cpp @@ -6,26 +6,23 @@ // README-INCLUDE gate, so the doc code can never drift from working code. // region: relay -#include - #include +#include using namespace signalwire::relay; int main() { - auto client = RelayClient::from_env(); + auto client = RelayClient::from_env(); - client.on_call([](Call& call) { - call.answer(); - auto action = call.play({ - {{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}} - }); - if (!action.wait()) { // false = call ended before playback finished - std::cerr << "playback interrupted\n"; - } - call.hangup(); - }); + client.on_call([](Call& call) { + call.answer(); + auto action = call.play({{{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}}}); + if (!action.wait()) { // false = call ended before playback finished + std::cerr << "playback interrupted\n"; + } + call.hangup(); + }); - client.run(); + client.run(); } // endregion: relay diff --git a/examples/quickstart_rest.cpp b/examples/quickstart_rest.cpp index 4c4c0e6..1e01055 100644 --- a/examples/quickstart_rest.cpp +++ b/examples/quickstart_rest.cpp @@ -8,22 +8,31 @@ // initializers), not JSON maps — the region is the real, compiled call shape. // region: rest +#include #include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { auto client = RestClient::from_env(); auto agents = client.fabric().ai_agents.list(); - auto call = client.calling().dial({ - .from = "+15559876543", .to = "+15551234567", + auto call = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", .url = "https://example.com/handler", }); auto numbers = client.phone_numbers().search({{"areacode", "512"}}); auto results = client.datasphere().documents.search({ .query_string = "billing policy", }); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } // endregion: rest diff --git a/examples/receptionist_agent_example.cpp b/examples/receptionist_agent_example.cpp index 1bf47a7..eae101a 100644 --- a/examples/receptionist_agent_example.cpp +++ b/examples/receptionist_agent_example.cpp @@ -1,23 +1,30 @@ // Copyright (c) 2025 SignalWire — MIT License // Receptionist prefab: department routing with call transfer. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { prefabs::ReceptionistAgent agent("receptionist", "/receptionist"); agent.set_greeting("Welcome to Acme Corporation! How may I direct your call?"); - agent.set_departments({ - {"sales", {{"number", "+15551001"}, {"description", "Sales and new accounts"}}}, - {"support", {{"number", "+15551002"}, {"description", "Technical support"}}}, - {"billing", {{"number", "+15551003"}, {"description", "Billing and payments"}}}, - {"hr", {{"number", "+15551004"}, {"description", "Human resources"}}} - }); + agent.set_departments( + {{"sales", {{"number", "+15551001"}, {"description", "Sales and new accounts"}}}, + {"support", {{"number", "+15551002"}, {"description", "Technical support"}}}, + {"billing", {{"number", "+15551003"}, {"description", "Billing and payments"}}}, + {"hr", {{"number", "+15551004"}, {"description", "Human resources"}}}}); agent.set_transfer_message("I'll transfer you now. Please hold."); std::cout << "Receptionist at http://0.0.0.0:3000/receptionist\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/record_call_example.cpp b/examples/record_call_example.cpp index a3b9ba8..4ec9ea2 100644 --- a/examples/record_call_example.cpp +++ b/examples/record_call_example.cpp @@ -1,36 +1,45 @@ // Copyright (c) 2025 SignalWire — MIT License // Record call demo: tools that start/stop call recording. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("recorder", "/recorder"); agent.prompt_add_section("Role", "You are an agent that can record calls."); - agent.prompt_add_section("Instructions", "", { - "Always ask for consent before recording", - "Use start_recording to begin and stop_recording to end" - }); + agent.prompt_add_section("Instructions", "", + {"Always ask for consent before recording", + "Use start_recording to begin and stop_recording to end"}); agent.define_tool("start_recording", "Start recording the call", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Recording started.") - .record_call("rec-001", true, "wav", "both", "", false, 44.0); - }); + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + return swaig::FunctionResult("Recording started.") + .record_call("rec-001", true, "wav", "both", "", false, 44.0); + }); - agent.define_tool("stop_recording", "Stop recording the call", + agent.define_tool( + "stop_recording", "Stop recording the call", {{"type", "object"}, {"properties", json::object()}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Recording stopped.") - .stop_record_call("rec-001"); + (void)args; + (void)raw; + return swaig::FunctionResult("Recording stopped.").stop_record_call("rec-001"); }); std::cout << "Record call demo at http://0.0.0.0:3000/recorder\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/relay_answer_and_welcome.cpp b/examples/relay_answer_and_welcome.cpp index a7632a5..c11dcf0 100644 --- a/examples/relay_answer_and_welcome.cpp +++ b/examples/relay_answer_and_welcome.cpp @@ -19,42 +19,40 @@ // Build: // cmake --build build --target example_relay_answer_and_welcome -#include #include +#include using namespace signalwire::relay; using json = nlohmann::json; int main() { - auto client = RelayClient::from_env(); - - client.on_call([](Call& call) { - std::cout << "Inbound call from " << call.from() << "\n"; - - // Answer the call. - call.answer(); - - // Play a TTS greeting and wait for it to finish. wait() returns false - // if the call ends before playback completes. - auto action = call.play({ - json{ - {"type", "tts"}, - {"params", json{{"text", "Welcome to SignalWire! How can I help you today?"}}}, - } - }); - if (!action.wait()) { - std::cout << "Greeting interrupted (caller hung up early)\n"; - } - - // Hang up cleanly. wait_for_ended() returns false on timeout. - call.hangup(); - if (call.wait_for_ended(10000)) { - std::cout << "Call ended\n"; - } else { - std::cout << "Timed out waiting for call end\n"; - } - }); - - std::cout << "Waiting for inbound calls...\n"; - client.run(); + auto client = RelayClient::from_env(); + + client.on_call([](Call& call) { + std::cout << "Inbound call from " << call.from() << "\n"; + + // Answer the call. + call.answer(); + + // Play a TTS greeting and wait for it to finish. wait() returns false + // if the call ends before playback completes. + auto action = call.play({json{ + {"type", "tts"}, + {"params", json{{"text", "Welcome to SignalWire! How can I help you today?"}}}, + }}); + if (!action.wait()) { + std::cout << "Greeting interrupted (caller hung up early)\n"; + } + + // Hang up cleanly. wait_for_ended() returns false on timeout. + call.hangup(); + if (call.wait_for_ended(10000)) { + std::cout << "Call ended\n"; + } else { + std::cout << "Timed out waiting for call end\n"; + } + }); + + std::cout << "Waiting for inbound calls...\n"; + client.run(); } diff --git a/examples/relay_audit_harness.cpp b/examples/relay_audit_harness.cpp index db33deb..f77febd 100644 --- a/examples/relay_audit_harness.cpp +++ b/examples/relay_audit_harness.cpp @@ -23,12 +23,11 @@ // - 0 on a clean handshake + subscribe + event dispatch // - 1 on any error (socket failure, handshake timeout, no event in 5s) -#include - #include #include #include #include +#include #include #include #include @@ -40,30 +39,35 @@ using json = nlohmann::json; namespace { std::string env_or(const char* name, const std::string& fallback) { - const char* v = std::getenv(name); - return (v && *v) ? std::string(v) : fallback; + const char* v = std::getenv(name); + return (v && *v) ? std::string(v) : fallback; } std::vector split_csv(const std::string& s) { - std::vector out; - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, ',')) { - // trim whitespace - size_t a = item.find_first_not_of(" \t"); - size_t b = item.find_last_not_of(" \t"); - if (a == std::string::npos) continue; - out.push_back(item.substr(a, b - a + 1)); + std::vector out; + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, ',')) { + // trim whitespace + size_t a = item.find_first_not_of(" \t"); + size_t b = item.find_last_not_of(" \t"); + if (a == std::string::npos) { + continue; } - return out; + out.push_back(item.substr(a, b - a + 1)); + } + return out; } } // namespace int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { // Quiet logging so the audit's stdout/stderr capture stays small. if (!std::getenv("SIGNALWIRE_LOG_MODE")) { - ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); + ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); } const std::string project = env_or("SIGNALWIRE_PROJECT_ID", "audit"); @@ -86,41 +90,43 @@ int main() { // audit_relay_handshake.py: `state.event_dispatched = True` branch). std::atomic saw_event{false}; client.on_event([&](const relay::RelayEvent& ev) { - saw_event.store(true); - try { - client.send_raw_request("signalwire.event", json{ - {"dispatched", true}, - {"event_type", ev.event_type}, - {"echoed", ev.params}, - }); - } catch (...) { - // Audit only needs the saw_event flag; failure to ack is fine. - } + saw_event.store(true); + try { + client.send_raw_request("signalwire.event", json{ + {"dispatched", true}, + {"event_type", ev.event_type}, + {"echoed", ev.params}, + }); + // The ack is best-effort: this harness only reports whether an event was + // OBSERVED (the saw_event flag above), so a failed echo must not change + // the audit result or abort the callback. Deliberately empty. + // NOLINTNEXTLINE(bugprone-empty-catch) + } catch (...) { + } }); if (!client.connect()) { - std::cerr << "relay_audit_harness: connect failed\n"; - return 1; + std::cerr << "relay_audit_harness: connect failed\n"; + return 1; } // Explicit signalwire.subscribe so the audit fixture sees the method // name (its watcher only marks subscribe_seen on a literal // `signalwire.subscribe` frame). try { - client.send_raw_request( - "signalwire.subscribe", - json{{"contexts", cfg.contexts}} - ); + client.send_raw_request("signalwire.subscribe", json{{"contexts", cfg.contexts}}); } catch (const std::exception& e) { - std::cerr << "relay_audit_harness: subscribe failed: " << e.what() << "\n"; - return 1; + std::cerr << "relay_audit_harness: subscribe failed: " << e.what() << "\n"; + return 1; } // Wait up to 5 seconds for an inbound event. auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (std::chrono::steady_clock::now() < deadline) { - if (saw_event.load()) break; - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + if (saw_event.load()) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } bool got = saw_event.load(); @@ -130,10 +136,14 @@ int main() { client.disconnect(); if (!got) { - std::cerr << "relay_audit_harness: no event arrived within 5s\n"; - return 1; + std::cerr << "relay_audit_harness: no event arrived within 5s\n"; + return 1; } std::cout << "relay_audit_harness: event dispatched\n"; return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/relay_demo.cpp b/examples/relay_demo.cpp index 7386184..bcada8e 100644 --- a/examples/relay_demo.cpp +++ b/examples/relay_demo.cpp @@ -1,30 +1,28 @@ // Copyright (c) 2025 SignalWire — MIT License // RELAY client demo: answer inbound calls and play TTS. -#include #include +#include using namespace signalwire::relay; int main() { - auto client = RelayClient::from_env(); + auto client = RelayClient::from_env(); - client.on_call([](Call& call) { - std::cout << "Inbound call: " << call.call_id() << "\n"; - call.answer(); + client.on_call([](Call& call) { + std::cout << "Inbound call: " << call.call_id() << "\n"; + call.answer(); - auto action = call.play({ - {{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}} - }); - if (!action.wait()) { - std::cout << "playback interrupted (call ended early)\n"; - } + auto action = call.play({{{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire!"}}}}}); + if (!action.wait()) { + std::cout << "playback interrupted (call ended early)\n"; + } - call.hangup(); - std::cout << "Call ended\n"; - }); + call.hangup(); + std::cout << "Call ended\n"; + }); - std::cout << "RELAY demo running\n"; - std::cout << "Set SIGNALWIRE_PROJECT_ID, SIGNALWIRE_API_TOKEN, SIGNALWIRE_SPACE\n"; - client.run(); + std::cout << "RELAY demo running\n"; + std::cout << "Set SIGNALWIRE_PROJECT_ID, SIGNALWIRE_API_TOKEN, SIGNALWIRE_SPACE\n"; + client.run(); } diff --git a/examples/rest_audit_harness.cpp b/examples/rest_audit_harness.cpp index b1c6b2a..6b82464 100644 --- a/examples/rest_audit_harness.cpp +++ b/examples/rest_audit_harness.cpp @@ -21,12 +21,11 @@ // - fabric.subscribers.list GET /api/fabric/subscribers // - compatibility.calls.list GET /api/laml/2010-04-01/Accounts/{proj}/Calls.json -#include -#include - #include #include #include +#include +#include #include #include @@ -36,46 +35,55 @@ using json = nlohmann::json; namespace { void die(const std::string& msg) { - std::cerr << "rest_audit_harness: " << msg << "\n"; - std::exit(1); + std::cerr << "rest_audit_harness: " << msg << "\n"; + std::exit(1); } std::string env_required(const char* name) { - const char* v = std::getenv(name); - if (!v || !*v) { - die(std::string(name) + " env var required"); - } - return std::string(v); + const char* v = std::getenv(name); + if (!v || !*v) { + die(std::string(name) + " env var required"); + } + return std::string(v); } std::string env_or(const char* name, const std::string& fallback = "") { - const char* v = std::getenv(name); - return (v && *v) ? std::string(v) : fallback; + const char* v = std::getenv(name); + return (v && *v) ? std::string(v) : fallback; } /// Convert a JSON value to a string (for query-string params). std::string to_query_string(const json& v) { - if (v.is_string()) return v.get(); - if (v.is_number()) return v.dump(); - if (v.is_boolean()) return v.get() ? "true" : "false"; + if (v.is_string()) { + return v.get(); + } + if (v.is_number()) { return v.dump(); + } + if (v.is_boolean()) { + return v.get() ? "true" : "false"; + } + return v.dump(); } std::map args_to_query_map(const json& args) { - std::map m; - if (args.is_object()) { - for (auto it = args.begin(); it != args.end(); ++it) { - m[it.key()] = to_query_string(it.value()); - } + std::map m; + if (args.is_object()) { + for (auto it = args.begin(); it != args.end(); ++it) { + m[it.key()] = to_query_string(it.value()); } - return m; + } + return m; } } // namespace int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { if (!std::getenv("SIGNALWIRE_LOG_MODE")) { - ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); + ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); } const std::string operation = env_required("REST_OPERATION"); @@ -86,34 +94,37 @@ int main() { json args; try { - args = json::parse(args_raw); + args = json::parse(args_raw); } catch (const json::parse_error& e) { - die(std::string("REST_OPERATION_ARGS not JSON: ") + e.what()); + die(std::string("REST_OPERATION_ARGS not JSON: ") + e.what()); } rest::RestClient client = rest::RestClient::with_base_url(fixture_url, project, token); json result; try { - if (operation == "calling.list_calls" || - operation == "compatibility.calls.list") { - // Compat / LAML calls listing — Twilio-shape path. - std::string path = "/api/laml/2010-04-01/Accounts/" + project + "/Calls.json"; - result = client.http_client().get(path, args_to_query_map(args)); - } else if (operation == "messaging.send") { - std::string path = "/api/laml/2010-04-01/Accounts/" + project + "/Messages.json"; - result = client.http_client().post(path, args); - } else if (operation == "phone_numbers.list") { - result = client.phone_numbers().list(args_to_query_map(args)); - } else if (operation == "fabric.subscribers.list") { - result = client.fabric().subscribers.list(args_to_query_map(args)); - } else { - die("unsupported REST_OPERATION: " + operation); - } + if (operation == "calling.list_calls" || operation == "compatibility.calls.list") { + // Compat / LAML calls listing — Twilio-shape path. + std::string path = "/api/laml/2010-04-01/Accounts/" + project + "/Calls.json"; + result = client.http_client().get(path, args_to_query_map(args)); + } else if (operation == "messaging.send") { + std::string path = "/api/laml/2010-04-01/Accounts/" + project + "/Messages.json"; + result = client.http_client().post(path, args); + } else if (operation == "phone_numbers.list") { + result = client.phone_numbers().list(args_to_query_map(args)); + } else if (operation == "fabric.subscribers.list") { + result = client.fabric().subscribers.list(args_to_query_map(args)); + } else { + die("unsupported REST_OPERATION: " + operation); + } } catch (const std::exception& e) { - die(std::string(operation) + " failed: " + e.what()); + die(std::string(operation) + " failed: " + e.what()); } std::cout << result.dump() << "\n"; return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/rest_demo.cpp b/examples/rest_demo.cpp index 17950a1..efd7b53 100644 --- a/examples/rest_demo.cpp +++ b/examples/rest_demo.cpp @@ -1,36 +1,43 @@ // Copyright (c) 2025 SignalWire — MIT License // REST client demo: manage resources, place calls. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // List AI agents - std::cout << "Listing AI agents...\n"; - auto agents = client.fabric().ai_agents.list(); - std::cout << " Found: " << agents.dump(2) << "\n"; + // List AI agents + std::cout << "Listing AI agents...\n"; + auto agents = client.fabric().ai_agents.list(); + std::cout << " Found: " << agents.dump(2) << "\n"; - // Search phone numbers - std::cout << "\nSearching phone numbers...\n"; - auto numbers = client.phone_numbers().search({{"areacode", "512"}}); - std::cout << " Results: " << numbers.dump(2) << "\n"; + // Search phone numbers + std::cout << "\nSearching phone numbers...\n"; + auto numbers = client.phone_numbers().search({{"areacode", "512"}}); + std::cout << " Results: " << numbers.dump(2) << "\n"; - // Place a test call - std::cout << "\nPlacing test call...\n"; - auto result = client.calling().dial({ - .from = "+15559876543", - .to = "+15551234567", - .url = "https://example.com/handler", - }); - std::cout << " Call: " << result.dump(2) << "\n"; + // Place a test call + std::cout << "\nPlacing test call...\n"; + auto result = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", + .url = "https://example.com/handler", + }); + std::cout << " Call: " << result.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "REST error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "REST error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/room_and_sip_example.cpp b/examples/room_and_sip_example.cpp index c907aa8..3523189 100644 --- a/examples/room_and_sip_example.cpp +++ b/examples/room_and_sip_example.cpp @@ -1,51 +1,57 @@ // Copyright (c) 2025 SignalWire — MIT License // Room/SIP demo: join video rooms and SIP refer. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("room-sip", "/room-sip"); agent.prompt_add_section("Role", "You manage video rooms and SIP routing."); agent.enable_sip_routing(true); agent.register_sip_username("conference"); - agent.define_tool("join_meeting", "Join a video room", - {{"type", "object"}, {"properties", { - {"room", {{"type", "string"}, {"description", "Room name"}}} - }}}, + agent.define_tool( + "join_meeting", "Join a video room", + {{"type", "object"}, + {"properties", {{"room", {{"type", "string"}, {"description", "Room name"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string room = args.value("room", "default"); - return swaig::FunctionResult("Joining room: " + room) - .join_room(room); + (void)raw; + std::string room = args.value("room", "default"); + return swaig::FunctionResult("Joining room: " + room).join_room(room); }); - agent.define_tool("sip_transfer", "Transfer via SIP REFER", - {{"type", "object"}, {"properties", { - {"uri", {{"type", "string"}, {"description", "SIP URI"}}} - }}}, + agent.define_tool( + "sip_transfer", "Transfer via SIP REFER", + {{"type", "object"}, + {"properties", {{"uri", {{"type", "string"}, {"description", "SIP URI"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string uri = args.value("uri", ""); - return swaig::FunctionResult("Transferring via SIP to " + uri) - .sip_refer(uri); + (void)raw; + std::string uri = args.value("uri", ""); + return swaig::FunctionResult("Transferring via SIP to " + uri).sip_refer(uri); }); - agent.define_tool("start_conference", "Start a conference call", - {{"type", "object"}, {"properties", { - {"name", {{"type", "string"}, {"description", "Conference name"}}} - }}}, + agent.define_tool( + "start_conference", "Start a conference call", + {{"type", "object"}, + {"properties", {{"name", {{"type", "string"}, {"description", "Conference name"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string name = args.value("name", "default-conf"); - return swaig::FunctionResult("Starting conference: " + name) - .join_conference(name, false, "true"); + (void)raw; + std::string name = args.value("name", "default-conf"); + return swaig::FunctionResult("Starting conference: " + name) + .join_conference(name, false, "true"); }); std::cout << "Room/SIP demo at http://0.0.0.0:3000/room-sip\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/session_and_state_demo.cpp b/examples/session_and_state_demo.cpp index 0973985..c63adb9 100644 --- a/examples/session_and_state_demo.cpp +++ b/examples/session_and_state_demo.cpp @@ -1,50 +1,53 @@ // Copyright (c) 2025 SignalWire — MIT License // Session state management with global data and callbacks. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("stateful", "/stateful"); agent.prompt_add_section("Role", "You are a stateful assistant that remembers context."); - agent.prompt_add_section("Instructions", "", { - "Track conversation topics in global data", - "Use session tokens for secure tool calls" - }); + agent.prompt_add_section( + "Instructions", "", + {"Track conversation topics in global data", "Use session tokens for secure tool calls"}); - agent.set_global_data({ - {"session_type", "demo"}, - {"interaction_count", 0} - }); + agent.set_global_data({{"session_type", "demo"}, {"interaction_count", 0}}); // Summary callback agent.on_summary([](const json& summary, const json& raw) { - (void)raw; - std::cout << "Conversation summary: " << summary.dump(2) << "\n"; + (void)raw; + std::cout << "Conversation summary: " << summary.dump(2) << "\n"; }); // Debug event callback agent.enable_debug_events(true); - agent.on_debug_event([](const json& event) { - std::cout << "Debug: " << event.dump() << "\n"; - }); + agent.on_debug_event([](const json& event) { std::cout << "Debug: " << event.dump() << "\n"; }); // Tool that updates state - agent.define_tool("update_topic", "Track the current topic", - {{"type", "object"}, {"properties", { - {"topic", {{"type", "string"}, {"description", "Current topic"}}} - }}}, + agent.define_tool( + "update_topic", "Track the current topic", + {{"type", "object"}, + {"properties", {{"topic", {{"type", "string"}, {"description", "Current topic"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string topic = args.value("topic", "general"); - return swaig::FunctionResult("Topic updated to: " + topic) - .update_global_data({{"current_topic", topic}}) - .set_metadata({{"last_topic_update", topic}}); - }, true /* secure */); + (void)raw; + std::string topic = args.value("topic", "general"); + return swaig::FunctionResult("Topic updated to: " + topic) + .update_global_data({{"current_topic", topic}}) + .set_metadata({{"last_topic_update", topic}}); + }, + true /* secure */); std::cout << "Session state demo at http://0.0.0.0:3000/stateful\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/simple_agent.cpp b/examples/simple_agent.cpp index 44df967..dd97652 100644 --- a/examples/simple_agent.cpp +++ b/examples/simple_agent.cpp @@ -1,82 +1,90 @@ // Copyright (c) 2025 SignalWire — MIT License // Simple agent demonstrating POM prompts, SWAIG tools, hints, and languages. -#include #include +#include +#include using namespace signalwire; using json = nlohmann::json; class SimpleAgent : public agent::AgentBase { -public: - SimpleAgent() : AgentBase("simple", "/simple") { - // Structured prompt via POM - prompt_add_section("Personality", "You are a friendly and helpful assistant."); - prompt_add_section("Goal", "Help users with basic tasks and answer questions."); - prompt_add_section("Instructions", "", { - "Be concise and direct in your responses.", - "If you don't know something, say so clearly.", - "Use the get_time function when asked about the current time.", - "Use the get_weather function when asked about the weather." - }); + public: + SimpleAgent() : AgentBase("simple", "/simple") { + // Structured prompt via POM + prompt_add_section("Personality", "You are a friendly and helpful assistant."); + prompt_add_section("Goal", "Help users with basic tasks and answer questions."); + prompt_add_section( + "Instructions", "", + {"Be concise and direct in your responses.", "If you don't know something, say so clearly.", + "Use the get_time function when asked about the current time.", + "Use the get_weather function when asked about the weather."}); - // LLM parameters - set_prompt_llm_params({ - {"temperature", 0.3}, {"top_p", 0.9}, - {"barge_confidence", 0.7}, {"presence_penalty", 0.1} - }); + // LLM parameters + set_prompt_llm_params({{"temperature", 0.3}, + {"top_p", 0.9}, + {"barge_confidence", 0.7}, + {"presence_penalty", 0.1}}); - // Post-prompt for summary - set_post_prompt("Return a JSON summary: {\"topic\": \"...\", \"satisfied\": true/false}"); + // Post-prompt for summary + set_post_prompt("Return a JSON summary: {\"topic\": \"...\", \"satisfied\": true/false}"); - // Hints and pronunciation - add_hints({"SignalWire", "SWML", "SWAIG"}); - add_pronunciation("API", "A P I", false); - add_pronunciation("SIP", "sip", true); + // Hints and pronunciation + add_hints({"SignalWire", "SWML", "SWAIG"}); + add_pronunciation("API", "A P I", false); + add_pronunciation("SIP", "sip", true); - // Languages - add_language({"English", "en-US", "inworld.Mark"}); - add_language({"Spanish", "es", "inworld.Sarah"}); + // Languages + add_language({"English", "en-US", "inworld.Mark"}); + add_language({"Spanish", "es", "inworld.Sarah"}); - // AI parameters - set_params({{"ai_model", "gpt-4.1-nano"}, {"wait_for_user", false}}); + // AI parameters + set_params({{"ai_model", "gpt-4.1-nano"}, {"wait_for_user", false}}); - // Global data - set_global_data({{"company_name", "SignalWire"}, {"product", "AI Agent SDK"}}); + // Global data + set_global_data({{"company_name", "SignalWire"}, {"product", "AI Agent SDK"}}); - // Native functions - set_native_functions({"check_time", "wait_seconds"}); + // Native functions + set_native_functions({"check_time", "wait_seconds"}); - // SIP routing - enable_sip_routing(true); - register_sip_username("simple_agent"); + // SIP routing + enable_sip_routing(true); + register_sip_username("simple_agent"); - // Tools - define_tool("get_time", "Get the current time", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - auto t = std::time(nullptr); - char buf[32]; - std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&t)); - return swaig::FunctionResult(std::string("The current time is ") + buf); - }); + // Tools + define_tool("get_time", "Get the current time", + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + auto t = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", std::localtime(&t)); + return swaig::FunctionResult(std::string("The current time is ") + buf); + }); - define_tool("get_weather", "Get weather for a location", - {{"type", "object"}, {"properties", { - {"location", {{"type", "string"}, {"description", "City name"}}} - }}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string loc = args.value("location", "Unknown"); - return swaig::FunctionResult("It's sunny and 72F in " + loc + ".") - .update_global_data({{"weather_location", loc}}); - }); - } + define_tool( + "get_weather", "Get weather for a location", + {{"type", "object"}, + {"properties", {{"location", {{"type", "string"}, {"description", "City name"}}}}}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)raw; + std::string loc = args.value("location", "Unknown"); + return swaig::FunctionResult("It's sunny and 72F in " + loc + ".") + .update_global_data({{"weather_location", loc}}); + }); + } }; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { SimpleAgent agent; std::cout << "Starting simple agent at http://0.0.0.0:3000/simple\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/simple_dynamic_agent.cpp b/examples/simple_dynamic_agent.cpp index a1d8963..15b9762 100644 --- a/examples/simple_dynamic_agent.cpp +++ b/examples/simple_dynamic_agent.cpp @@ -1,12 +1,16 @@ // Copyright (c) 2025 SignalWire — MIT License // Dynamic agent: per-request customization via DynamicConfigCallback. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("dynamic", "/dynamic"); agent.prompt_add_section("Role", "You are a customer support agent."); @@ -14,23 +18,26 @@ int main() { // Dynamic config: customize per request based on query params agent.set_dynamic_config_callback( - [](const std::map& query, - const json& body, - const std::map& headers, - agent::AgentBase& copy) { - (void)body; (void)headers; - auto it = query.find("tenant"); - if (it != query.end()) { - copy.prompt_add_section("Tenant", "You work for " + it->second + "."); - copy.set_global_data({{"tenant", it->second}}); - } - auto lang = query.find("lang"); - if (lang != query.end() && lang->second == "es") { - copy.add_language({"Spanish", "es", "inworld.Sarah"}); - } + [](const std::map& query, const json& body, + const std::map& headers, agent::AgentBase& copy) { + (void)body; + (void)headers; + auto it = query.find("tenant"); + if (it != query.end()) { + copy.prompt_add_section("Tenant", "You work for " + it->second + "."); + copy.set_global_data({{"tenant", it->second}}); + } + auto lang = query.find("lang"); + if (lang != query.end() && lang->second == "es") { + copy.add_language({"Spanish", "es", "inworld.Sarah"}); + } }); std::cout << "Dynamic agent at http://0.0.0.0:3000/dynamic\n"; std::cout << "Try: ?tenant=AcmeCorp&lang=es\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/simple_dynamic_enhanced.cpp b/examples/simple_dynamic_enhanced.cpp index c2491cc..265fc9d 100644 --- a/examples/simple_dynamic_enhanced.cpp +++ b/examples/simple_dynamic_enhanced.cpp @@ -2,110 +2,106 @@ // Enhanced dynamic agent adapting based on request parameters: // ?vip=true, ?department=sales, ?customer_id=X, ?language=es -#include #include +#include +#include using namespace signalwire; using json = nlohmann::json; static std::string to_lower(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), ::tolower); - return s; + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + return s; } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase a("dynamic-enhanced", "/dynamic-enhanced"); - a.set_dynamic_config_callback([]( - const std::map& qp, - const json& body, - const std::map& headers, - agent::AgentBase& ephemeral) { - - (void)body; (void)headers; - - auto get = [&qp](const std::string& key, const std::string& def) { - auto it = qp.find(key); - return (it != qp.end()) ? it->second : def; - }; - - bool is_vip = to_lower(get("vip", "")) == "true"; - std::string department = to_lower(get("department", "general")); - std::string customer_id = get("customer_id", ""); - std::string lang = to_lower(get("language", "en")); - - // Voice and language - std::string voice = is_vip ? "inworld.Sarah" : "inworld.Mark"; - if (lang == "es") { - ephemeral.add_language({"Spanish", "es-ES", voice}); - } else { - ephemeral.add_language({"English", "en-US", voice}); - } - - // AI parameters - ephemeral.set_params({ - {"end_of_speech_timeout", is_vip ? 300 : 500}, - {"attention_timeout", is_vip ? 20000 : 15000} - }); - - // Global data - json global = { - {"department", department}, - {"service_level", is_vip ? "vip" : "standard"} - }; - if (!customer_id.empty()) global["customer_id"] = customer_id; - ephemeral.set_global_data(global); - - // Role prompt - std::string role = customer_id.empty() - ? "You are a professional customer service representative." - : "You are a customer service rep helping customer " + customer_id + "."; - if (is_vip) role += " This is a VIP customer who receives priority service."; - ephemeral.prompt_add_section("Role", role); - - // Department expertise - if (department == "sales") { - ephemeral.prompt_add_section("Sales Expertise", "You specialize in sales:", { - "Present product features and benefits", - "Handle pricing questions", - "Process orders and upgrades" - }); - ephemeral.add_hints({"pricing", "enterprise", "upgrade"}); - } else if (department == "billing") { - ephemeral.prompt_add_section("Billing Expertise", "You specialize in billing:", { - "Explain statements and charges", - "Process payment arrangements", - "Handle dispute resolution" - }); - ephemeral.add_hints({"invoice", "payment", "charges"}); - } else { - ephemeral.prompt_add_section("Support Guidelines", "Follow these principles:", { - "Listen carefully to customer needs", - "Provide accurate information", - "Escalate complex issues when appropriate" - }); - ephemeral.add_hints({"support", "troubleshoot", "help"}); - } - - if (is_vip) { - ephemeral.prompt_add_section("VIP Standards", "Premium service:", { - "Provide immediate attention", - "Offer exclusive options", - "Ensure complete satisfaction" - }); - } - - // Common tool - ephemeral.define_tool("check_order", "Check order status", - {{"type", "object"}, {"properties", { - {"order_number", {{"type", "string"}, {"description", "Order number"}}} - }}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string num = args.value("order_number", "unknown"); - return swaig::FunctionResult( - "Order " + num + " is being processed. Ships in 2 business days."); - }); + a.set_dynamic_config_callback([](const std::map& qp, const json& body, + const std::map& headers, + agent::AgentBase& ephemeral) { + (void)body; + (void)headers; + + auto get = [&qp](const std::string& key, const std::string& def) { + auto it = qp.find(key); + return (it != qp.end()) ? it->second : def; + }; + + bool is_vip = to_lower(get("vip", "")) == "true"; + std::string department = to_lower(get("department", "general")); + std::string customer_id = get("customer_id", ""); + std::string lang = to_lower(get("language", "en")); + + // Voice and language + std::string voice = is_vip ? "inworld.Sarah" : "inworld.Mark"; + if (lang == "es") { + ephemeral.add_language({"Spanish", "es-ES", voice}); + } else { + ephemeral.add_language({"English", "en-US", voice}); + } + + // AI parameters + ephemeral.set_params({{"end_of_speech_timeout", is_vip ? 300 : 500}, + {"attention_timeout", is_vip ? 20000 : 15000}}); + + // Global data + json global = {{"department", department}, {"service_level", is_vip ? "vip" : "standard"}}; + if (!customer_id.empty()) { + global["customer_id"] = customer_id; + } + ephemeral.set_global_data(global); + + // Role prompt + std::string role = + customer_id.empty() + ? "You are a professional customer service representative." + : "You are a customer service rep helping customer " + customer_id + "."; + if (is_vip) { + role += " This is a VIP customer who receives priority service."; + } + ephemeral.prompt_add_section("Role", role); + + // Department expertise + if (department == "sales") { + ephemeral.prompt_add_section("Sales Expertise", "You specialize in sales:", + {"Present product features and benefits", + "Handle pricing questions", "Process orders and upgrades"}); + ephemeral.add_hints({"pricing", "enterprise", "upgrade"}); + } else if (department == "billing") { + ephemeral.prompt_add_section("Billing Expertise", "You specialize in billing:", + {"Explain statements and charges", + "Process payment arrangements", "Handle dispute resolution"}); + ephemeral.add_hints({"invoice", "payment", "charges"}); + } else { + ephemeral.prompt_add_section( + "Support Guidelines", "Follow these principles:", + {"Listen carefully to customer needs", "Provide accurate information", + "Escalate complex issues when appropriate"}); + ephemeral.add_hints({"support", "troubleshoot", "help"}); + } + + if (is_vip) { + ephemeral.prompt_add_section("VIP Standards", "Premium service:", + {"Provide immediate attention", "Offer exclusive options", + "Ensure complete satisfaction"}); + } + + // Common tool + ephemeral.define_tool( + "check_order", "Check order status", + {{"type", "object"}, + {"properties", + {{"order_number", {{"type", "string"}, {"description", "Order number"}}}}}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)raw; + std::string num = args.value("order_number", "unknown"); + return swaig::FunctionResult("Order " + num + + " is being processed. Ships in 2 business days."); + }); }); std::cout << "Enhanced Dynamic Agent at http://0.0.0.0:3000/dynamic-enhanced\n"; @@ -114,4 +110,8 @@ int main() { std::cout << " ?customer_id=X Personalized experience\n"; std::cout << " ?language=es Spanish\n"; a.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/simple_static_agent.cpp b/examples/simple_static_agent.cpp index 1d5bba8..af6eaec 100644 --- a/examples/simple_static_agent.cpp +++ b/examples/simple_static_agent.cpp @@ -1,11 +1,15 @@ // Copyright (c) 2025 SignalWire — MIT License // Simple static agent: no tools, just a prompt-driven conversation. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("static-agent", "/static"); agent.set_prompt_text( @@ -20,4 +24,8 @@ int main() { std::cout << "Static agent at http://0.0.0.0:3000/static\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/skills_audit_harness.cpp b/examples/skills_audit_harness.cpp index bf12ec6..cc7d0b5 100644 --- a/examples/skills_audit_harness.cpp +++ b/examples/skills_audit_harness.cpp @@ -28,16 +28,15 @@ // upstream" via real bytes on the wire. The DataMap webhook URL contains // `${args.foo}` placeholders that we expand from SKILL_HANDLER_ARGS. -#include -#include -#include -#include -#include - #include #include #include #include +#include +#include +#include +#include +#include #include #include @@ -47,13 +46,13 @@ using json = nlohmann::json; namespace { void die(const std::string& msg) { - std::cerr << "skills_audit_harness: " << msg << "\n"; - std::exit(1); + std::cerr << "skills_audit_harness: " << msg << "\n"; + std::exit(1); } std::string env_or(const char* name, const std::string& fallback = "") { - const char* v = std::getenv(name); - return (v && *v) ? std::string(v) : fallback; + const char* v = std::getenv(name); + return (v && *v) ? std::string(v) : fallback; } /// Expand DataMap webhook templates of the form `${args.foo}` / @@ -61,39 +60,43 @@ std::string env_or(const char* name, const std::string& fallback = "") { /// (`${response.foo}`, `${global_data.x}`) are left intact — they're /// platform-side runtime variables the audit fixture doesn't care about. std::string expand_template(const std::string& tmpl, const json& args) { - static const std::regex re(R"(\$\{([^}]+)\})"); - std::string out; - auto begin = std::sregex_iterator(tmpl.begin(), tmpl.end(), re); - auto end = std::sregex_iterator(); - size_t last = 0; - for (auto it = begin; it != end; ++it) { - out.append(tmpl, last, it->position() - last); - std::string inner = (*it)[1]; + static const std::regex re(R"(\$\{([^}]+)\})"); + std::string out; + auto begin = std::sregex_iterator(tmpl.begin(), tmpl.end(), re); + auto end = std::sregex_iterator(); + size_t last = 0; + for (auto it = begin; it != end; ++it) { + out.append(tmpl, last, it->position() - last); + std::string inner = (*it)[1]; - // Strip optional `lc:` / `enc:` prefixes — we don't transform - // for the audit; the captured wire path is what the fixture sees. - std::string body = inner; - while (true) { - if (body.compare(0, 3, "lc:") == 0) body = body.substr(3); - else if (body.compare(0, 4, "enc:") == 0) body = body.substr(4); - else break; - } + // Strip optional `lc:` / `enc:` prefixes — we don't transform + // for the audit; the captured wire path is what the fixture sees. + std::string body = inner; + while (true) { + if (body.compare(0, 3, "lc:") == 0) { + body = body.substr(3); + } else if (body.compare(0, 4, "enc:") == 0) { + body = body.substr(4); + } else { + break; + } + } - if (body.compare(0, 5, "args.") == 0) { - std::string field = body.substr(5); - if (args.contains(field) && args[field].is_string()) { - out += url_encode(args[field].get()); - } else if (args.contains(field)) { - out += args[field].dump(); - } - } else { - // Leave unknown placeholders in place. - out += "${" + inner + "}"; - } - last = it->position() + it->length(); + if (body.compare(0, 5, "args.") == 0) { + std::string field = body.substr(5); + if (args.contains(field) && args[field].is_string()) { + out += url_encode(args[field].get()); + } else if (args.contains(field)) { + out += args[field].dump(); + } + } else { + // Leave unknown placeholders in place. + out += "${" + inner + "}"; } - out.append(tmpl, last, std::string::npos); - return out; + last = it->position() + it->length(); + } + out.append(tmpl, last, std::string::npos); + return out; } /// Apply SKILL_FIXTURE_URL override to a webhook URL: replace the @@ -102,205 +105,229 @@ std::string expand_template(const std::string& tmpl, const json& args) { /// path-substring assertion (`trivia`, `current.json`, etc.) still /// matches. std::string redirect_to_fixture(const std::string& url, const std::string& fixture) { - if (fixture.empty()) return url; - auto scheme_end = url.find("://"); - if (scheme_end == std::string::npos) return url; - auto path_start = url.find('/', scheme_end + 3); - std::string fix = fixture; - while (!fix.empty() && fix.back() == '/') fix.pop_back(); - if (path_start == std::string::npos) { - return fix; - } - return fix + url.substr(path_start); + if (fixture.empty()) { + return url; + } + auto scheme_end = url.find("://"); + if (scheme_end == std::string::npos) { + return url; + } + auto path_start = url.find('/', scheme_end + 3); + std::string fix = fixture; + while (!fix.empty() && fix.back() == '/') { + fix.pop_back(); + } + if (path_start == std::string::npos) { + return fix; + } + return fix + url.substr(path_start); } /// Look up a tool handler from a skill's tool list by name. -const swaig::ToolDefinition* find_tool( - const std::vector& tools, - const std::string& name) { - for (const auto& t : tools) { - if (t.name == name) return &t; +const swaig::ToolDefinition* find_tool(const std::vector& tools, + const std::string& name) { + for (const auto& t : tools) { + if (t.name == name) { + return &t; } - return nullptr; + } + return nullptr; } /// Build skill construction params for skills that need credentials. json build_skill_params(const std::string& skill_name) { - json p = json::object(); - if (skill_name == "web_search") { - // Audit sets GOOGLE_API_KEY / GOOGLE_CSE_ID + WEB_SEARCH_BASE_URL. - const std::string k = env_or("GOOGLE_API_KEY"); - if (!k.empty()) p["api_key"] = k; - const std::string c = env_or("GOOGLE_CSE_ID"); - if (!c.empty()) p["search_engine_id"] = c; - } else if (skill_name == "datasphere") { - // Audit sets DATASPHERE_TOKEN + DATASPHERE_BASE_URL. Plug - // synthetic project_id / space_name / document_id so setup() - // validates; the actual upstream call uses DATASPHERE_BASE_URL. - p["space_name"] = "audit-space"; - p["project_id"] = "audit-project"; - p["document_id"] = "audit-doc"; - const std::string t = env_or("DATASPHERE_TOKEN"); - if (!t.empty()) p["token"] = t; - } else if (skill_name == "weather_api") { - const std::string k = env_or("WEATHER_API_KEY"); - if (!k.empty()) p["api_key"] = k; - } else if (skill_name == "api_ninjas_trivia") { - const std::string k = env_or("API_NINJAS_KEY"); - if (!k.empty()) p["api_key"] = k; + json p = json::object(); + if (skill_name == "web_search") { + // Audit sets GOOGLE_API_KEY / GOOGLE_CSE_ID + WEB_SEARCH_BASE_URL. + const std::string k = env_or("GOOGLE_API_KEY"); + if (!k.empty()) { + p["api_key"] = k; } - return p; + const std::string c = env_or("GOOGLE_CSE_ID"); + if (!c.empty()) { + p["search_engine_id"] = c; + } + } else if (skill_name == "datasphere") { + // Audit sets DATASPHERE_TOKEN + DATASPHERE_BASE_URL. Plug + // synthetic project_id / space_name / document_id so setup() + // validates; the actual upstream call uses DATASPHERE_BASE_URL. + p["space_name"] = "audit-space"; + p["project_id"] = "audit-project"; + p["document_id"] = "audit-doc"; + const std::string t = env_or("DATASPHERE_TOKEN"); + if (!t.empty()) { + p["token"] = t; + } + } else if (skill_name == "weather_api") { + const std::string k = env_or("WEATHER_API_KEY"); + if (!k.empty()) { + p["api_key"] = k; + } + } else if (skill_name == "api_ninjas_trivia") { + const std::string k = env_or("API_NINJAS_KEY"); + if (!k.empty()) { + p["api_key"] = k; + } + } + return p; } /// Drive a handler-based skill. The skill's handler issues real HTTP to /// the upstream pointed at by the per-skill base-URL env var. -int run_handler_skill(skills::SkillBase& skill, - const std::string& tool_name, - const json& args) { - auto tools = skill.register_tools(); - const auto* tool = find_tool(tools, tool_name); - if (!tool) { - die("skill did not register expected tool: " + tool_name); - } - if (!tool->handler) { - die("tool '" + tool_name + "' has no handler"); - } +int run_handler_skill(skills::SkillBase& skill, const std::string& tool_name, const json& args) { + auto tools = skill.register_tools(); + const auto* tool = find_tool(tools, tool_name); + if (!tool) { + die("skill did not register expected tool: " + tool_name); + } + if (!tool->handler) { + die("tool '" + tool_name + "' has no handler"); + } - swaig::FunctionResult r = tool->handler(args, json::object()); - // FunctionResult doesn't expose a getter for the response string — - // serialize and lift it from the JSON. The audit's sentinel check - // looks at the harness's stdout, so we just need to emit the parsed - // SWAIG result. - std::cout << r.to_json().dump() << "\n"; - return 0; + swaig::FunctionResult r = tool->handler(args, json::object()); + // FunctionResult doesn't expose a getter for the response string — + // serialize and lift it from the JSON. The audit's sentinel check + // looks at the harness's stdout, so we just need to emit the parsed + // SWAIG result. + std::cout << r.to_json().dump() << "\n"; + return 0; } /// Drive a DataMap skill. We extract the webhook URL from the skill's /// registered DataMap, expand `${args.foo}` placeholders, redirect at /// the fixture, and execute the GET ourselves — this is what the /// SignalWire platform does in production for DataMap-based tools. -int run_datamap_skill(skills::SkillBase& skill, - const std::string& tool_name, - const json& args) { - auto datamaps = skill.get_datamap_functions(); - json dm_func; - for (const auto& d : datamaps) { - if (d.value("function", "") == tool_name) { - dm_func = d; - break; - } - } - if (dm_func.empty()) { - die("skill did not register DataMap for tool: " + tool_name); +int run_datamap_skill(skills::SkillBase& skill, const std::string& tool_name, const json& args) { + auto datamaps = skill.get_datamap_functions(); + json dm_func; + for (const auto& d : datamaps) { + if (d.value("function", "") == tool_name) { + dm_func = d; + break; } + } + if (dm_func.empty()) { + die("skill did not register DataMap for tool: " + tool_name); + } - if (!dm_func.contains("data_map") || - !dm_func["data_map"].contains("webhooks") || - !dm_func["data_map"]["webhooks"].is_array() || - dm_func["data_map"]["webhooks"].empty()) { - die("DataMap '" + tool_name + "' has no webhook block"); - } - const auto& webhook = dm_func["data_map"]["webhooks"][0]; - const std::string url_template = webhook.value("url", ""); - const std::string method = webhook.value("method", "GET"); + if (!dm_func.contains("data_map") || !dm_func["data_map"].contains("webhooks") || + !dm_func["data_map"]["webhooks"].is_array() || dm_func["data_map"]["webhooks"].empty()) { + die("DataMap '" + tool_name + "' has no webhook block"); + } + const auto& webhook = dm_func["data_map"]["webhooks"][0]; + const std::string url_template = webhook.value("url", ""); + const std::string method = webhook.value("method", "GET"); - std::map headers; - if (webhook.contains("headers") && webhook["headers"].is_object()) { - for (auto it = webhook["headers"].begin(); it != webhook["headers"].end(); ++it) { - if (it.value().is_string()) { - headers[it.key()] = it.value().get(); - } - } + std::map headers; + if (webhook.contains("headers") && webhook["headers"].is_object()) { + for (auto it = webhook["headers"].begin(); it != webhook["headers"].end(); ++it) { + if (it.value().is_string()) { + headers[it.key()] = it.value().get(); + } } + } - std::string url = expand_template(url_template, args); - std::string fixture = env_or("SKILL_FIXTURE_URL"); - url = redirect_to_fixture(url, fixture); + std::string url = expand_template(url_template, args); + std::string fixture = env_or("SKILL_FIXTURE_URL"); + url = redirect_to_fixture(url, fixture); - skills::SkillHttpResponse resp; - if (method == "POST" || method == "PUT") { - resp = skills::http_post(url, "", "application/json", headers); - } else { - resp = skills::http_get(url, headers); - } - if (resp.status == 0) { - die("DataMap HTTP failed: " + resp.error); - } + skills::SkillHttpResponse resp; + if (method == "POST" || method == "PUT") { + resp = skills::http_post(url, "", "application/json", headers); + } else { + resp = skills::http_get(url, headers); + } + if (resp.status == 0) { + die("DataMap HTTP failed: " + resp.error); + } - json parsed; - try { - parsed = json::parse(resp.body); - } catch (...) { - parsed = resp.body; - } - json out = json::object({ - {"status", resp.status}, - {"url", url}, - {"body", parsed}, - }); - std::cout << out.dump() << "\n"; - return 0; + json parsed; + try { + parsed = json::parse(resp.body); + } catch (...) { + parsed = resp.body; + } + json out = json::object({ + {"status", resp.status}, + {"url", url}, + {"body", parsed}, + }); + std::cout << out.dump() << "\n"; + return 0; } } // namespace int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { if (!std::getenv("SIGNALWIRE_LOG_MODE")) { - ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); + ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); } const std::string skill_name = env_or("SKILL_NAME"); - if (skill_name.empty()) die("SKILL_NAME required"); + if (skill_name.empty()) { + die("SKILL_NAME required"); + } const std::string args_raw = env_or("SKILL_HANDLER_ARGS", "{}"); json args; try { - args = json::parse(args_raw); + args = json::parse(args_raw); } catch (const json::parse_error& e) { - die(std::string("SKILL_HANDLER_ARGS not JSON: ") + e.what()); + die(std::string("SKILL_HANDLER_ARGS not JSON: ") + e.what()); } skills::ensure_builtin_skills_registered(); auto& reg = skills::SkillRegistry::instance(); if (!reg.has_skill(skill_name)) { - die("skill not registered: " + skill_name); + die("skill not registered: " + skill_name); } auto skill = reg.create(skill_name); - if (!skill) die("failed to create skill: " + skill_name); + if (!skill) { + die("failed to create skill: " + skill_name); + } json params = build_skill_params(skill_name); if (!skill->setup(params)) { - die("skill setup() returned false"); + die("skill setup() returned false"); } if (skill_name == "web_search") { - return run_handler_skill(*skill, "web_search", args); + return run_handler_skill(*skill, "web_search", args); } if (skill_name == "wikipedia_search") { - return run_handler_skill(*skill, "search_wiki", args); + return run_handler_skill(*skill, "search_wiki", args); } if (skill_name == "datasphere") { - return run_handler_skill(*skill, "search_knowledge", args); + return run_handler_skill(*skill, "search_knowledge", args); } if (skill_name == "spider") { - return run_handler_skill(*skill, "scrape_url", args); + return run_handler_skill(*skill, "scrape_url", args); } if (skill_name == "weather_api") { - return run_datamap_skill(*skill, "get_weather", args); + return run_datamap_skill(*skill, "get_weather", args); } if (skill_name == "api_ninjas_trivia") { - // The audit doesn't pass `category`; the upstream endpoint - // accepts a wildcard request without one. Synthesize a default - // so the URL template still expands cleanly. - json effective = args; - if (!effective.is_object()) effective = json::object(); - if (!effective.contains("category")) { - effective["category"] = "general"; - } - return run_datamap_skill(*skill, "get_trivia", effective); + // The audit doesn't pass `category`; the upstream endpoint + // accepts a wildcard request without one. Synthesize a default + // so the URL template still expands cleanly. + json effective = args; + if (!effective.is_object()) { + effective = json::object(); + } + if (!effective.contains("category")) { + effective["category"] = "general"; + } + return run_datamap_skill(*skill, "get_trivia", effective); } die("unsupported skill: " + skill_name); return 1; // unreachable + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/skills_demo.cpp b/examples/skills_demo.cpp index fa7e310..45e0c27 100644 --- a/examples/skills_demo.cpp +++ b/examples/skills_demo.cpp @@ -1,11 +1,15 @@ // Copyright (c) 2025 SignalWire — MIT License // Skills system demo: one-liner skill injection. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("Multi-Skill Assistant", "/assistant"); agent.add_language({"English", "en-US", "inworld.Mark"}); @@ -14,17 +18,21 @@ int main() { agent.add_skill("math"); // Web search with custom params - agent.add_skill("web_search", { - {"api_key", "your-google-api-key"}, - {"search_engine_id", "your-engine-id"}, - {"num_results", 1} - }); + agent.add_skill("web_search", {{"api_key", "your-google-api-key"}, + {"search_engine_id", "your-engine-id"}, + {"num_results", 1}}); auto skills = agent.list_skills(); std::cout << "Loaded skills:"; - for (const auto& s : skills) std::cout << " " << s; + for (const auto& s : skills) { + std::cout << " " << s; + } std::cout << "\n"; std::cout << "Skills demo at http://0.0.0.0:3000/assistant\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/step_function_inheritance_demo.cpp b/examples/step_function_inheritance_demo.cpp index 7ee076c..30c882b 100644 --- a/examples/step_function_inheritance_demo.cpp +++ b/examples/step_function_inheritance_demo.cpp @@ -36,41 +36,36 @@ // Run this file to see the rendered SWML — there are no real webhook // endpoints behind the tools, this is purely a documentation example. +#include #include #include -#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("step_function_inheritance_demo", "/"); // Register three SWAIG tools so we have something to whitelist. // In a real agent these would call out to webhooks; here they're // stubs. - agent.define_tool( - "lookup_account", - "Look up customer account details by account number", - {{"account_number", {{"type", "string"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("looked up"); - }); + agent.define_tool("lookup_account", "Look up customer account details by account number", + {{"account_number", {{"type", "string"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("looked up"); + }); - agent.define_tool( - "process_payment", - "Process a payment for the current customer", - {{"amount", {{"type", "number"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("payment processed"); - }); + agent.define_tool("process_payment", "Process a payment for the current customer", + {{"amount", {{"type", "number"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { + return swaig::FunctionResult("payment processed"); + }); agent.define_tool( - "send_receipt", - "Email a receipt to the customer", - {{"email", {{"type", "string"}}}}, - [](const nlohmann::json&, const nlohmann::json&) { - return swaig::FunctionResult("sent"); - }); + "send_receipt", "Email a receipt to the customer", {{"email", {{"type", "string"}}}}, + [](const nlohmann::json&, const nlohmann::json&) { return swaig::FunctionResult("sent"); }); // Build the contexts. auto& cb = agent.define_contexts(); @@ -123,6 +118,10 @@ int main() { // exactly which steps have a `functions` key in the output and // which don't. auto swml = agent.render_swml(); - std::cout << swml.dump(2) << std::endl; + std::cout << swml.dump(2) << '\n'; return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/survey_agent_example.cpp b/examples/survey_agent_example.cpp index 8799615..36be422 100644 --- a/examples/survey_agent_example.cpp +++ b/examples/survey_agent_example.cpp @@ -1,22 +1,33 @@ // Copyright (c) 2025 SignalWire — MIT License // Survey prefab: typed survey with validation. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { prefabs::SurveyAgent agent("satisfaction-survey", "/survey"); agent.set_intro_message("Welcome to our customer satisfaction survey!"); - agent.set_questions({ - {{"key", "rating"}, {"question", "On a scale of 1-10, how satisfied are you?"}, {"type", "integer"}}, - {{"key", "recommend"}, {"question", "Would you recommend us to a friend?"}, {"type", "boolean"}}, - {{"key", "feedback"}, {"question", "Any additional feedback?"}, {"type", "string"}} - }); + agent.set_questions( + {{{"key", "rating"}, + {"question", "On a scale of 1-10, how satisfied are you?"}, + {"type", "integer"}}, + {{"key", "recommend"}, + {"question", "Would you recommend us to a friend?"}, + {"type", "boolean"}}, + {{"key", "feedback"}, {"question", "Any additional feedback?"}, {"type", "string"}}}); agent.set_completion_message("Thank you for completing our survey!"); std::cout << "Survey at http://0.0.0.0:3000/survey\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/swaig_features_agent.cpp b/examples/swaig_features_agent.cpp index b85e45e..b81e824 100644 --- a/examples/swaig_features_agent.cpp +++ b/examples/swaig_features_agent.cpp @@ -1,74 +1,85 @@ // Copyright (c) 2025 SignalWire — MIT License // Comprehensive SWAIG features: all FunctionResult action types. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("swaig-features", "/swaig-features"); agent.prompt_add_section("Role", "You demonstrate all SWAIG function result actions."); // Tool: state management - agent.define_tool("manage_state", "Update conversation state", - {{"type", "object"}, {"properties", { - {"key", {{"type", "string"}, {"description", "State key"}}}, - {"value", {{"type", "string"}, {"description", "State value"}}} - }}}, + agent.define_tool( + "manage_state", "Update conversation state", + {{"type", "object"}, + {"properties", + {{"key", {{"type", "string"}, {"description", "State key"}}}, + {"value", {{"type", "string"}, {"description", "State value"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - return swaig::FunctionResult("State updated") - .update_global_data({{args.value("key", "k"), args.value("value", "v")}}) - .set_metadata({{"last_update", "now"}}); + (void)raw; + return swaig::FunctionResult("State updated") + .update_global_data({{args.value("key", "k"), args.value("value", "v")}}) + .set_metadata({{"last_update", "now"}}); }); // Tool: media actions - agent.define_tool("play_music", "Play background music", - {{"type", "object"}, {"properties", { - {"url", {{"type", "string"}, {"description", "Audio URL"}}} - }}}, + agent.define_tool( + "play_music", "Play background music", + {{"type", "object"}, + {"properties", {{"url", {{"type", "string"}, {"description", "Audio URL"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - return swaig::FunctionResult("Playing music") - .play_background_file(args.value("url", ""), false); + (void)raw; + return swaig::FunctionResult("Playing music") + .play_background_file(args.value("url", ""), false); }); // Tool: speech control agent.define_tool("adjust_speech", "Adjust speech settings", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Settings adjusted") - .set_end_of_speech_timeout(2000) - .set_speech_event_timeout(5000) - .add_dynamic_hints({{"hints", json::array({"SignalWire", "SWML"})}}); - }); + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + return swaig::FunctionResult("Settings adjusted") + .set_end_of_speech_timeout(2000) + .set_speech_event_timeout(5000) + .add_dynamic_hints({{"hints", json::array({"SignalWire", "SWML"})}}); + }); // Tool: context switching - agent.define_tool("switch", "Switch conversation context", - {{"type", "object"}, {"properties", { - {"prompt", {{"type", "string"}, {"description", "New system prompt"}}} - }}}, + agent.define_tool( + "switch", "Switch conversation context", + {{"type", "object"}, + {"properties", {{"prompt", {{"type", "string"}, {"description", "New system prompt"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - return swaig::FunctionResult("Switching context") - .switch_context(args.value("prompt", ""), "", true, false); + (void)raw; + return swaig::FunctionResult("Switching context") + .switch_context(args.value("prompt", ""), "", true, false); }); // Tool: SMS agent.define_tool("send_text", "Send an SMS", - {{"type", "object"}, {"properties", { - {"to", {{"type", "string"}, {"description", "Phone number"}}}, - {"message", {{"type", "string"}, {"description", "Text message"}}} - }}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - return swaig::FunctionResult("Text sent") - .send_sms(args.value("to", ""), "+15559876543", args.value("message", "")); - }); + {{"type", "object"}, + {"properties", + {{"to", {{"type", "string"}, {"description", "Phone number"}}}, + {"message", {{"type", "string"}, {"description", "Text message"}}}}}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)raw; + return swaig::FunctionResult("Text sent") + .send_sms(args.value("to", ""), "+15559876543", + args.value("message", "")); + }); std::cout << "SWAIG features at http://0.0.0.0:3000/swaig-features\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/swml_service_example.cpp b/examples/swml_service_example.cpp index 5840701..e30e870 100644 --- a/examples/swml_service_example.cpp +++ b/examples/swml_service_example.cpp @@ -1,12 +1,15 @@ // Copyright (c) 2025 SignalWire — MIT License // Low-level SWML Service: build SWML documents directly with verbs. -#include #include +#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { swml::Service svc; svc.set_route("/swml-service"); svc.set_port(3000); @@ -14,10 +17,8 @@ int main() { // Build a simple IVR flow svc.answer({{"max_duration", 3600}}); svc.play({{"url", "https://example.com/greeting.mp3"}}); - svc.ai({ - {"prompt", {{"text", "You are a helpful assistant."}}}, - {"post_prompt", {{"text", "Summarize the conversation."}}} - }); + svc.ai({{"prompt", {{"text", "You are a helpful assistant."}}}, + {"post_prompt", {{"text", "Summarize the conversation."}}}}); svc.hangup(); // Print the document @@ -26,4 +27,8 @@ int main() { std::cout << "SWML Service at http://0.0.0.0:3000/swml-service\n"; svc.serve(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/swml_service_routing_example.cpp b/examples/swml_service_routing_example.cpp index a9c0cb3..87ddce4 100644 --- a/examples/swml_service_routing_example.cpp +++ b/examples/swml_service_routing_example.cpp @@ -1,30 +1,29 @@ // Copyright (c) 2025 SignalWire — MIT License // SWML Service with routing: multiple sections and goto/label verbs. -#include #include +#include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { swml::Service svc; svc.set_route("/routed"); svc.set_port(3000); // Main section: answer and route svc.answer(); - svc.prompt({ - {"play", "https://example.com/menu.mp3"}, - {"speech", {{"hints", json::array({"sales", "support", "billing"})}}}, - {"digits", {{"max", 1}, {"terminators", "#"}}} - }); + svc.prompt({{"play", "https://example.com/menu.mp3"}, + {"speech", {{"hints", json::array({"sales", "support", "billing"})}}}, + {"digits", {{"max", 1}, {"terminators", "#"}}}}); // Conditional routing - svc.cond({{ - {{"when", "prompt_value == '1' or prompt_value == 'sales'"}, - {"then", json::array({{{"goto_section", "sales"}}})}} - }}); + svc.cond({{{{"when", "prompt_value == '1' or prompt_value == 'sales'"}, + {"then", json::array({{{"goto_section", "sales"}}})}}}}); // Named sections svc.add_verb("sales", "play", {{"url", "https://example.com/sales.mp3"}}); @@ -35,4 +34,8 @@ int main() { std::cout << "Routed SWML:\n" << swml.dump(2) << "\n\n"; std::cout << "SWML routing at http://0.0.0.0:3000/routed\n"; svc.serve(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/swmlservice_ai_sidecar.cpp b/examples/swmlservice_ai_sidecar.cpp index 82ea137..3fb3694 100644 --- a/examples/swmlservice_ai_sidecar.cpp +++ b/examples/swmlservice_ai_sidecar.cpp @@ -28,15 +28,15 @@ // bin/swaig-test http://user:pass@localhost:3000/sales-sidecar // --exec lookup_competitor --param competitor=ACME -#include -#include - -#include "httplib.h" - #include #include +#include +#include +#include #include +#include "httplib.h" + using namespace signalwire; using json = nlohmann::json; @@ -46,66 +46,76 @@ using json = nlohmann::json; /// so we mount the route through the `register_additional_routes` virtual /// hook — the same extension point AgentBase uses for /post_prompt etc. class SalesSidecar : public swml::Service { -public: - SalesSidecar() = default; + public: + SalesSidecar() = default; -protected: - void register_additional_routes(httplib::Server& server) override { - // POST //events — ai_sidecar POSTs each lifecycle / transcription - // event as JSON. Reply with 200 and any non-redirect body. - const std::string events_path = route() + "/events"; - server.Post(events_path.c_str(), - [](const httplib::Request& req, httplib::Response& res) { - json body; - try { body = json::parse(req.body); } - catch (...) { body = json::object(); } - const auto type = body.value("type", std::string{""}); - std::cout << "[sidecar event] type=" << type - << " body=" << body.dump() << std::endl; - res.set_content("{\"ok\":true}", "application/json"); - }); - } + protected: + void register_additional_routes(httplib::Server& server) override { + // POST //events — ai_sidecar POSTs each lifecycle / transcription + // event as JSON. Reply with 200 and any non-redirect body. + const std::string events_path = route() + "/events"; + server.Post(events_path, [](const httplib::Request& req, httplib::Response& res) { + json body; + try { + body = json::parse(req.body); + } catch (...) { + body = json::object(); + } + const auto type = body.value("type", std::string{""}); + std::cout << "[sidecar event] type=" << type << " body=" << body.dump() << '\n'; + res.set_content("{\"ok\":true}", "application/json"); + }); + } }; int main(int argc, char** argv) { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { int port = 3000; std::string public_url = "https://your-host.example.com/sales-sidecar"; if (argc > 1) { - int p = std::atoi(argv[1]); - if (p > 0) port = p; + int p = 0; + try { + p = std::stoi(argv[1]); + } catch (const std::exception&) { + p = 0; + } + if (p > 0) { + port = p; + } } if (argc > 2) { - public_url = argv[2]; + public_url = argv[2]; } SalesSidecar svc; - svc.set_name("sales-sidecar") - .set_route("/sales-sidecar") - .set_port(port); + svc.set_name("sales-sidecar").set_route("/sales-sidecar").set_port(port); // 1. Emit any SWML — including ai_sidecar. Service::add_verb accepts // arbitrary verb dicts, so new platform verbs work without an SDK // release. Schema: `prompt` and `lang` are required; SWAIG.defaults // points the sidecar's LLM at this service's /swaig route. svc.answer(); - svc.add_verb("main", "ai_sidecar", json::object({ - {"prompt", - "You are a real-time sales copilot. Listen to the call and surface " - "competitor pricing comparisons when relevant."}, - {"lang", "en-US"}, - {"direction", json::array({"remote-caller", "local-caller"})}, - // Where the sidecar POSTs lifecycle/transcription events. - // Optional — drop this key if you don't need an event sink. - {"url", public_url + "/events"}, - // Where the sidecar's LLM POSTs SWAIG tool calls. The Service's - // built-in /swaig route is what answers them. Note the UPPERCASE - // SWAIG key — the platform schema is case-sensitive here. - {"SWAIG", json::object({ - {"defaults", json::object({ - {"web_hook_url", public_url + "/swaig"}, - })}, - })}, - })); + svc.add_verb("main", "ai_sidecar", + json::object({ + {"prompt", + "You are a real-time sales copilot. Listen to the call and surface " + "competitor pricing comparisons when relevant."}, + {"lang", "en-US"}, + {"direction", json::array({"remote-caller", "local-caller"})}, + // Where the sidecar POSTs lifecycle/transcription events. + // Optional — drop this key if you don't need an event sink. + {"url", public_url + "/events"}, + // Where the sidecar's LLM POSTs SWAIG tool calls. The Service's + // built-in /swaig route is what answers them. Note the UPPERCASE + // SWAIG key — the platform schema is case-sensitive here. + {"SWAIG", json::object({ + {"defaults", json::object({ + {"web_hook_url", public_url + "/swaig"}, + })}, + })}, + })); svc.hangup(); // 2. Register tools the sidecar's LLM can call. Same `define_tool` @@ -115,23 +125,24 @@ int main(int argc, char** argv) { /*description=*/ "Look up competitor pricing by company name. The sidecar should call " "this whenever the caller mentions a competitor.", - /*parameters=*/json::object({ + /*parameters=*/ + json::object({ {"type", "object"}, - {"properties", json::object({ - {"competitor", json::object({ - {"type", "string"}, - {"description", "The competitor's company name, e.g. 'ACME'."}, - })}, - })}, + {"properties", + json::object({ + {"competitor", json::object({ + {"type", "string"}, + {"description", "The competitor's company name, e.g. 'ACME'."}, + })}, + })}, {"required", json::array({"competitor"})}, }), - /*handler=*/[](const json& args, const json& /*raw*/) -> swaig::FunctionResult { - const std::string competitor = - args.value("competitor", std::string{""}); - return swaig::FunctionResult( - "Pricing for " + competitor + ": $99/seat. Our equivalent " - "plan is $79/seat with the same SLA." - ); + /*handler=*/ + [](const json& args, const json& /*raw*/) -> swaig::FunctionResult { + const std::string competitor = args.value("competitor", std::string{""}); + return swaig::FunctionResult("Pricing for " + competitor + + ": $99/seat. Our equivalent " + "plan is $79/seat with the same SLA."); }, /*secure=*/false, }); @@ -143,9 +154,15 @@ int main(int argc, char** argv) { << " or watch the [INFO] log line printed by serve() for\n" << " the auto-generated user / password.\n" << " Tools: "; - for (const auto& n : svc.list_tool_names()) std::cout << n << " "; + for (const auto& n : svc.list_tool_names()) { + std::cout << n << " "; + } std::cout << "\n\nSWML document:\n" << svc.render_swml().dump(2) << "\n"; svc.serve(); return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/swmlservice_swaig_standalone.cpp b/examples/swmlservice_swaig_standalone.cpp index 1219e9a..b9e5239 100644 --- a/examples/swmlservice_swaig_standalone.cpp +++ b/examples/swmlservice_swaig_standalone.cpp @@ -28,27 +28,35 @@ // bin/swaig-test http://user:pass@localhost:3000/standalone // --exec lookup_competitor --param competitor=ACME -#include -#include - #include #include +#include +#include +#include #include using namespace signalwire; using json = nlohmann::json; int main(int argc, char** argv) { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { int port = 3000; if (argc > 1) { - port = std::atoi(argv[1]); - if (port <= 0) port = 3000; + try { + port = std::stoi(argv[1]); + } catch (const std::exception&) { + port = 0; // fall through to the range check below + } + if (port <= 0) { + std::cerr << "port argument \"" << argv[1] << "\" is not usable; using 3000\n"; + port = 3000; + } } swml::Service svc; - svc.set_name("standalone-swaig") - .set_route("/standalone") - .set_port(port); + svc.set_name("standalone-swaig").set_route("/standalone").set_port(port); // 1. Build a minimal SWML document. Any verbs are fine — the SWAIG // HTTP surface is independent of what the document contains. @@ -63,22 +71,22 @@ int main(int argc, char** argv) { /*description=*/ "Look up competitor pricing by company name. Use this when the user " "asks how a competitor's price compares to ours.", - /*parameters=*/json::object({ + /*parameters=*/ + json::object({ {"type", "object"}, - {"properties", json::object({ - {"competitor", json::object({ - {"type", "string"}, - {"description", "The competitor's company name, e.g. 'ACME'."}, - })}, - })}, + {"properties", + json::object({ + {"competitor", json::object({ + {"type", "string"}, + {"description", "The competitor's company name, e.g. 'ACME'."}, + })}, + })}, {"required", json::array({"competitor"})}, }), - /*handler=*/[](const json& args, const json& /*raw*/) -> swaig::FunctionResult { - const std::string competitor = - args.value("competitor", std::string{""}); - return swaig::FunctionResult( - competitor + " pricing is $99/seat; we're $79/seat." - ); + /*handler=*/ + [](const json& args, const json& /*raw*/) -> swaig::FunctionResult { + const std::string competitor = args.value("competitor", std::string{""}); + return swaig::FunctionResult(competitor + " pricing is $99/seat; we're $79/seat."); }, /*secure=*/false, }); @@ -89,10 +97,17 @@ int main(int argc, char** argv) { << " or watch the [INFO] log line printed by serve() for\n" << " the auto-generated user / password.\n" << " Tools: "; - for (const auto& n : svc.list_tool_names()) std::cout << n << " "; + for (const auto& n : svc.list_tool_names()) { + std::cout << n << " "; + } std::cout << "\n\n" - << "SWML document:\n" << svc.render_swml().dump(2) << "\n"; + << "SWML document:\n" + << svc.render_swml().dump(2) << "\n"; svc.serve(); return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/tap_example.cpp b/examples/tap_example.cpp index e5559ed..0783001 100644 --- a/examples/tap_example.cpp +++ b/examples/tap_example.cpp @@ -1,35 +1,43 @@ // Copyright (c) 2025 SignalWire — MIT License // Tap demo: stream call audio to an external URI. +#include #include using namespace signalwire; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("tap-demo", "/tap-demo"); agent.prompt_add_section("Role", "You can tap call audio for monitoring."); - agent.define_tool("start_tap", "Start audio tap", - {{"type", "object"}, {"properties", { - {"uri", {{"type", "string"}, {"description", "RTP destination URI"}}} - }}}, + agent.define_tool( + "start_tap", "Start audio tap", + {{"type", "object"}, + {"properties", {{"uri", {{"type", "string"}, {"description", "RTP destination URI"}}}}}}, [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)raw; - std::string uri = args.value("uri", "rtp://monitor.example.com:5000"); - return swaig::FunctionResult("Tap started to " + uri) - .tap(uri, "tap-001", "both", "PCMU", 20); + (void)raw; + std::string uri = args.value("uri", "rtp://monitor.example.com:5000"); + return swaig::FunctionResult("Tap started to " + uri) + .tap(uri, "tap-001", "both", "PCMU", 20); }); agent.define_tool("stop_tap", "Stop audio tap", - {{"type", "object"}, {"properties", json::object()}}, - [](const json& args, const json& raw) -> swaig::FunctionResult { - (void)args; (void)raw; - return swaig::FunctionResult("Tap stopped") - .stop_tap("tap-001"); - }); + {{"type", "object"}, {"properties", json::object()}}, + [](const json& args, const json& raw) -> swaig::FunctionResult { + (void)args; + (void)raw; + return swaig::FunctionResult("Tap stopped").stop_tap("tap-001"); + }); std::cout << "Tap demo at http://0.0.0.0:3000/tap-demo\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/web_search_agent.cpp b/examples/web_search_agent.cpp index 4ff1429..6f996a3 100644 --- a/examples/web_search_agent.cpp +++ b/examples/web_search_agent.cpp @@ -1,26 +1,27 @@ // Copyright (c) 2025 SignalWire — MIT License // Web search agent using the web_search skill. +#include #include #include -#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("web-search", "/web-search"); agent.prompt_add_section("Role", "You are a research assistant with web search."); - agent.prompt_add_section("Instructions", "", { - "Search the web when users ask factual questions", - "Cite your sources when providing information" - }); + agent.prompt_add_section("Instructions", "", + {"Search the web when users ask factual questions", + "Cite your sources when providing information"}); - agent.add_skill("web_search", { - {"api_key", signalwire::get_env("GOOGLE_SEARCH_API_KEY")}, - {"search_engine_id", signalwire::get_env("GOOGLE_SEARCH_ENGINE_ID")}, - {"num_results", 3} - }); + agent.add_skill("web_search", + {{"api_key", signalwire::get_env("GOOGLE_SEARCH_API_KEY")}, + {"search_engine_id", signalwire::get_env("GOOGLE_SEARCH_ENGINE_ID")}, + {"num_results", 3}}); agent.add_skill("datetime"); agent.add_language({"English", "en-US", "inworld.Mark"}); @@ -28,4 +29,8 @@ int main() { std::cout << "Web search agent at http://0.0.0.0:3000/web-search\n"; std::cout << "Requires: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/web_search_multi_instance_demo.cpp b/examples/web_search_multi_instance_demo.cpp index f36f3c9..fb08a4a 100644 --- a/examples/web_search_multi_instance_demo.cpp +++ b/examples/web_search_multi_instance_demo.cpp @@ -2,20 +2,24 @@ // Web search skill with multiple instances (general, news, quick) plus Wikipedia. // Required: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID -#include #include +#include +#include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { const char* api_key = std::getenv("GOOGLE_SEARCH_API_KEY"); const char* engine_id = std::getenv("GOOGLE_SEARCH_ENGINE_ID"); agent::AgentBase agent("multi-search", "/multi-search"); agent.prompt_add_section("Role", - "You are a research assistant with access to multiple search tools. " - "Use the most appropriate tool for each query."); + "You are a research assistant with access to multiple search tools. " + "Use the most appropriate tool for each query."); agent.add_language({"English", "en-US", "inworld.Mark"}); agent.set_params({{"ai_model", "gpt-4.1-nano"}}); @@ -26,36 +30,32 @@ int main() { // Wikipedia search agent.add_skill("wikipedia_search", {{"num_results", 2}}); - if (!api_key || !engine_id || - std::string(api_key).empty() || std::string(engine_id).empty()) { - std::cout << "Warning: Missing GOOGLE_SEARCH_API_KEY or GOOGLE_SEARCH_ENGINE_ID.\n"; - std::cout << "Web search instances will not be available.\n"; + if (!api_key || !engine_id || std::string(api_key).empty() || std::string(engine_id).empty()) { + std::cout << "Warning: Missing GOOGLE_SEARCH_API_KEY or GOOGLE_SEARCH_ENGINE_ID.\n"; + std::cout << "Web search instances will not be available.\n"; } else { - // General web search (default tool name) - agent.add_skill("web_search", { - {"api_key", api_key}, - {"search_engine_id", engine_id}, - {"num_results", 3} - }); - - // News search - agent.add_skill("web_search", { - {"api_key", api_key}, - {"search_engine_id", engine_id}, - {"tool_name", "search_news"}, - {"num_results", 5} - }); - - // Quick single-result search - agent.add_skill("web_search", { - {"api_key", api_key}, - {"search_engine_id", engine_id}, - {"tool_name", "quick_search"}, - {"num_results", 1} - }); + // General web search (default tool name) + agent.add_skill("web_search", + {{"api_key", api_key}, {"search_engine_id", engine_id}, {"num_results", 3}}); + + // News search + agent.add_skill("web_search", {{"api_key", api_key}, + {"search_engine_id", engine_id}, + {"tool_name", "search_news"}, + {"num_results", 5}}); + + // Quick single-result search + agent.add_skill("web_search", {{"api_key", api_key}, + {"search_engine_id", engine_id}, + {"tool_name", "quick_search"}, + {"num_results", 1}}); } std::cout << "Multi-search agent at http://0.0.0.0:3000/multi-search\n"; std::cout << "Tools: web_search, search_news, quick_search, search_wiki\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/examples/wikipedia_demo.cpp b/examples/wikipedia_demo.cpp index 7865d04..6c1a8e1 100644 --- a/examples/wikipedia_demo.cpp +++ b/examples/wikipedia_demo.cpp @@ -1,18 +1,21 @@ // Copyright (c) 2025 SignalWire — MIT License // Wikipedia search agent using the wikipedia_search skill. +#include #include using namespace signalwire; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { agent::AgentBase agent("wikipedia", "/wikipedia"); agent.prompt_add_section("Role", "You are a knowledge assistant with Wikipedia access."); - agent.prompt_add_section("Instructions", "", { - "Search Wikipedia when users ask about topics", - "Provide concise summaries from Wikipedia articles" - }); + agent.prompt_add_section("Instructions", "", + {"Search Wikipedia when users ask about topics", + "Provide concise summaries from Wikipedia articles"}); agent.add_skill("wikipedia_search"); agent.add_skill("datetime"); @@ -20,4 +23,8 @@ int main() { std::cout << "Wikipedia agent at http://0.0.0.0:3000/wikipedia\n"; agent.run(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/include/signalwire/agent/agent_base.hpp b/include/signalwire/agent/agent_base.hpp index 721fec5..561864f 100644 --- a/include/signalwire/agent/agent_base.hpp +++ b/include/signalwire/agent/agent_base.hpp @@ -57,22 +57,19 @@ struct LanguageConfig { std::string voice; std::string engine; /// Explicit model name (e.g. "eleven_turbo_v2_5", "arcana"). Emitted as - /// the language object's ``model`` key only when non-empty — matches the - /// Python reference (add_language ``model=`` kwarg). + /// the language object's ``model`` key only when non-empty. std::string model; /// Speech fillers spoken for natural pacing. Emitted as /// ``speech_fillers`` when both speech+function fillers are set, or as - /// the deprecated ``fillers`` key when only one kind is present, matching - /// the reference add_language filler handling. + /// the deprecated ``fillers`` key when only one kind is present. std::vector speech_fillers; /// Filler phrases spoken while a function call is in flight. Emitted as /// ``function_fillers`` when paired with speech_fillers. std::vector function_fillers; /// Per-language params dict (engine-specific tuning, voice /// settings, etc.). Emitted as the language object's ``params`` - /// key in SWML only when non-empty — matches Python reference - /// commit 029ca6f. Treated as "unset" when null OR when an empty - /// object. + /// key in SWML only when non-empty. Treated as "unset" when null + /// OR when an empty object. json params; [[nodiscard]] json to_json() const { @@ -86,7 +83,7 @@ struct LanguageConfig { if (!model.empty()) { j["model"] = model; } - // Filler emission mirrors Python: both kinds -> the two explicit keys; + // Filler emission: both kinds -> the two explicit keys; // only one kind -> the deprecated combined ``fillers`` key. if (!speech_fillers.empty() && !function_fillers.empty()) { j["speech_fillers"] = speech_fillers; @@ -97,8 +94,7 @@ struct LanguageConfig { j["fillers"] = function_fillers; } // Only emit the params key when non-empty so we don't pollute - // SWML with empty objects (matches Python's - // ``if params:`` check). + // SWML with empty objects. if (params.is_object() && !params.empty()) { j["params"] = params; } @@ -144,25 +140,73 @@ struct SwaigQueryParam { // AgentBase // ============================================================================ +/// The AI-agent host: an HTTP endpoint that renders SWML and services SWAIG +/// function calls for a single agent. +/// +/// AgentBase extends ``swml::Service`` (the framework-free SWML endpoint) with +/// everything an AI agent adds on top of a plain SWML document: a prompt (raw +/// text or a POM section tree), SWAIG tool definitions, contexts/steps, +/// languages/hints/pronunciations, skills, and a post-prompt summary hook. A +/// GET/POST on the agent's ``route`` renders the ``ai`` verb via +/// ``render_swml_for_request``; POSTs to ``/swaig`` dispatch to the registered +/// tool handlers through ``on_function_call``; ``/post_prompt`` receives the +/// conversation summary; ``/debug_events`` receives the AI module's debug +/// webhook when ``enable_debug_routes`` is on. The same object can also run +/// without a server via ``handle_request`` (raw method/url/headers/body) or +/// ``handle_serverless_request`` (Lambda / GCF / Azure / CGI). +/// +/// ## Security — three independent mechanisms, all off-by-default-safe +/// +/// 1. **HTTP basic auth.** ``basic_auth`` (ctor) or ``set_auth`` gates every +/// mounted route; ``handle_request`` returns 401 for a bad or missing +/// credential. When no credentials are supplied the ``swml::Service`` base +/// generates a random pair rather than leaving the endpoint open. +/// 2. **Webhook signature validation.** When a signing key is resolved — +/// ``set_signing_key`` first, then the ``SIGNALWIRE_SIGNING_KEY`` env var — +/// the agent server auto-mounts the signature validator on POST ``/``, +/// ``/swaig``, and ``/post_prompt``: an unsigned or wrongly-signed request +/// gets a 403 and never reaches a handler. With no key resolved the agent +/// logs a startup warning and accepts unsigned POSTs. The URL the signature +/// is computed over honors ``X-Forwarded-Proto``/``X-Forwarded-Host`` only +/// when ``trust_proxy_for_signature(true)`` is set, because those headers +/// are caller-spoofable. +/// 3. **Per-call SWAIG tool tokens.** ``define_tool`` defaults ``secure`` to +/// TRUE. A secure tool's rendered ``web_hook_url`` +/// carries a ``__token=`` minted by this agent's ``SessionManager`` for the +/// (tool, call_id) pair (``create_tool_token``). The ``/swaig`` dispatcher +/// validates that token against the SWAIG body's ``call_id`` BEFORE +/// ``on_function_call`` is reached, so a replayed or cross-call token is +/// rejected and ``on_function_call`` is a post-validation hook — an override +/// must preserve that contract. This is why the protected +/// ``build_ai_verb``/``build_swaig_functions`` take ``call_id`` with NO +/// default — an omitted call_id would silently render every secure tool +/// without its token. +/// +/// Copy construction is supported (and used by ``clone()`` for the +/// per-request dynamic-config path); copy ASSIGNMENT is deleted. class AgentBase : public swml::Service { friend class signalwire::server::AgentServer; + // The serverless dispatchers extract the credential from their own envelope + // and then hand it to the SAME `swaig_validate_token` core the HTTP endpoint + // uses. They need reach into that protected core; granting it here keeps the + // decision in ONE place rather than duplicating it per transport. + friend struct signalwire::utils::ServerlessTokenAccess; public: /// Construct an agent. /// - /// Mirrors the reference ``AgentBase.__init__`` parameter-for-parameter. - /// Every parameter is FORWARDED to the same collaborator the reference - /// forwards it to, rather than merely stored: + /// Every parameter is FORWARDED to its collaborator, rather than merely + /// stored: /// * ``name`` / ``route`` / ``host`` / ``port`` / ``basic_auth`` / /// ``schema_path`` / ``config_file`` / ``schema_validation`` - /// → the ``swml::Service`` base (the reference's ``super().__init__``), + /// → the ``swml::Service`` base, /// * ``token_expiry_secs`` → the agent's ``SessionManager``, /// * ``config_file`` additionally seeds the ``service`` section that /// supplies name/route/host/port defaults (constructor arguments win). /// /// ``port`` is an optional so "not supplied" stays distinguishable from an /// explicit value — that is what lets the config file and the ``PORT`` env - /// var still apply, exactly as in the reference. + /// var still apply. explicit AgentBase( const std::string& name = "agent", const std::string& route = "/", const std::string& host = "0.0.0.0", const std::optional& port = std::nullopt, @@ -202,9 +246,10 @@ class AgentBase : public swml::Service { AgentBase& set_post_prompt_url(const std::string& url); AgentBase& prompt_add_section(const std::string& title, const std::string& body = "", const std::vector& bullets = {}); - AgentBase& prompt_add_subsection(const std::string& parent_title, const std::string& title, - const std::string& body = "", - const std::vector& bullets = {}); + /// ``bullets`` is ``nullopt`` when absent, which is treated as an empty list. + AgentBase& prompt_add_subsection( + const std::string& parent_title, const std::string& title, const std::string& body = "", + const std::optional>& bullets = std::nullopt); AgentBase& prompt_add_to_section(const std::string& title, const std::string& body = "", const std::vector& bullets = {}); [[nodiscard]] bool prompt_has_section(const std::string& title) const; @@ -213,43 +258,31 @@ class AgentBase : public swml::Service { /// Read-only snapshot of the agent's POM as a ``PromptObjectModel``. /// - /// Corresponds to ``agent.pom`` instance attribute (agent_base.py - /// line 209). Returns ``std::nullopt`` when ``use_pom`` is false - /// (mirroring Python's ``self.pom = None``); otherwise returns a - /// freshly built ``signalwire::pom::PromptObjectModel`` whose + /// Returns ``std::nullopt`` when ``use_pom`` is false; otherwise + /// returns a freshly built ``signalwire::pom::PromptObjectModel`` whose /// sections are deep-copied from the agent's internal section/ /// subsection structures so callers cannot mutate them in-place. [[nodiscard]] std::optional pom() const; /// Returns the post-prompt text whatever ``set_post_prompt`` stored, or - /// ``std::nullopt`` when no post-prompt has been set. - /// - /// Mirrors Python's ``PromptManager.get_post_prompt`` / - /// ``PromptMixin.get_post_prompt`` — used by SWML rendering when a - /// post-prompt is configured. + /// ``std::nullopt`` when no post-prompt has been set. Used by SWML + /// rendering when a post-prompt is configured. [[nodiscard]] std::optional get_post_prompt() const; /// Returns the raw prompt text whatever ``set_prompt_text`` stored, or /// ``std::nullopt`` when no raw prompt has been set. Distinct from /// ``get_prompt`` which renders the POM array when ``use_pom`` is /// true. - /// - /// Mirrors Python's ``PromptManager.get_raw_prompt``. [[nodiscard]] std::optional get_raw_prompt() const; /// Sets the prompt as a list of POM section JSON objects. Each /// section supports keys "title", "body", "bullets", "numbered", /// "numbered_bullets", and "subsections". Switches the agent to POM /// mode. - /// - /// Mirrors Python's ``PromptManager.set_prompt_pom``. AgentBase& set_prompt_pom(const std::vector& pom); /// Returns the contexts dictionary as a serialised JSON object, or /// ``std::nullopt`` when no contexts have been defined yet. - /// - /// Mirrors Python's ``PromptManager.get_contexts`` which returns the - /// contexts dict or ``None``. [[nodiscard]] std::optional get_contexts() const; // ======================================================================== @@ -313,52 +346,46 @@ class AgentBase : public swml::Service { // reference. on_function_call is overridden to add session-token // validation. AgentBase& define_tool(const swaig::ToolDefinition& tool); - /// ``secure`` defaults to TRUE (reference: ``tool_mixin.define_tool( - /// secure=True)``) — a tool defined without an explicit ``secure`` requires - /// SWAIG token validation. + /// ``secure`` defaults to TRUE — a tool defined without an explicit + /// ``secure`` requires SWAIG token validation. AgentBase& define_tool(const std::string& name, const std::string& description, const json& parameters, swaig::ToolHandler handler, bool secure = true); AgentBase& register_swaig_function(const json& func_def); + /// ``raw_data`` is OPTIONAL. Defaulted on the override too, so a call + /// through AgentBase& can omit it. [[nodiscard]] swaig::FunctionResult on_function_call(const std::string& name, const json& args, - const json& raw_data) override; + const json& raw_data = nullptr) override; [[nodiscard]] std::vector list_tools() const; - /// Register several SWAIG tools at once (Python: ``ToolMixin.define_tools``). + /// Register several SWAIG tools at once. /// Each entry is a full tool descriptor (name/description/parameters); /// delegates to ``register_swaig_function`` per entry. Returns ``*this`` for /// fluent chaining. AgentBase& define_tools(const std::vector& tools); - /// Register a routing callback for a request path (Python: - /// ``WebMixin.register_routing_callback``). The callback receives the parsed - /// request ``body`` and the request ``headers`` and returns the route to - /// dispatch to (empty string = no override), matching Python's - /// ``callback_fn(body, headers) -> route | None``. Used to steer inbound + /// Register a routing callback for a request path. The callback receives the + /// parsed request ``body`` and the request ``headers`` and returns the route + /// to dispatch to (empty string = no override). Used to steer inbound /// requests to per-path handlers. using RoutingCallback = std::function& headers)>; - AgentBase& register_routing_callback(RoutingCallback callback, const std::string& path = "/"); + AgentBase& register_routing_callback(RoutingCallback callback, const std::string& path = "/sip"); /// Install signal handlers so the agent's HTTP server drains + stops cleanly - /// on SIGINT/SIGTERM (Python: ``WebMixin.setup_graceful_shutdown``). Real - /// C++ implementation over the running httplib server. + /// on SIGINT/SIGTERM, over the running httplib server. void setup_graceful_shutdown(); /// Mint a per-call SWAIG-function token via the agent's SessionManager. /// - /// Corresponds to ``state_mixin.StateMixin._create_tool_token`` — - /// delegates to ``SessionManager::create_token`` and returns an empty - /// string on any thrown exception (Python catches all exceptions and - /// returns "" on error). + /// Delegates to ``SessionManager::create_token``. Returns an empty + /// string if token creation throws. [[nodiscard]] std::string create_tool_token(const std::string& tool_name, const std::string& call_id) const; /// Validate a per-call SWAIG-function token. Returns ``false`` when /// the function is not registered, when the SessionManager rejects the - /// token, or on any underlying exception. - /// - /// Corresponds to ``state_mixin.StateMixin.validate_tool_token`` — - /// rejects unknown function names up-front and swallows exceptions. + /// token, or on any underlying exception. Unknown function names are + /// rejected up-front, before the token is examined. [[nodiscard]] bool validate_tool_token(const std::string& function_name, const std::string& token, const std::string& call_id) const; @@ -368,11 +395,11 @@ class AgentBase : public swml::Service { AgentBase& add_hint(const std::string& hint); AgentBase& add_hints(const std::vector& hints); - /// Add a STRUCTURED pattern hint. Mirrors Python's - /// ``add_pattern_hint(hint, pattern, replace, ignore_case=False)``: appends - /// a ``{hint, pattern, replace, ignore_case}`` object to the hints list + /// Add a STRUCTURED pattern hint: appends a + /// ``{hint, pattern, replace, ignore_case}`` object to the hints list /// (not a bare string), which renders into the SWML ``ai.hints`` array. - /// No-op unless hint, pattern, and replace are all non-empty. + /// ``ignore_case`` defaults to false. No-op unless hint, pattern, and + /// replace are all non-empty. AgentBase& add_pattern_hint(const std::string& hint, const std::string& pattern, const std::string& replace, bool ignore_case = false); AgentBase& add_language(const LanguageConfig& lang); @@ -384,16 +411,12 @@ class AgentBase : public swml::Service { /// later (e.g. from a config loader). Passing an empty object /// removes the params key (treated as unset). No-op if ``code`` /// isn't found among previously-added languages. - /// - /// Corresponds to ``AIConfigMixin.set_language_params`` (029ca6f). AgentBase& set_language_params(const std::string& code, const json& params); /// Read the per-language ``params`` dict for a previously-added /// language. Returns ``std::nullopt`` when the code is unknown or - /// when params were never set on that language — no exception - /// path, mirroring Python's ``None`` return. - /// - /// Corresponds to ``AIConfigMixin.get_language_params`` (029ca6f). + /// when params were never set on that language — there is no + /// exception path. [[nodiscard]] std::optional get_language_params(const std::string& code) const; /// Configure ASR-driven multilingual mode (Mode B). Emits a top-level @@ -401,10 +424,7 @@ class AgentBase : public swml::Service { /// code-switching mode and the agent answers in whatever language the /// caller actually spoke. Mutually exclusive with set_languages() — if /// both are set the server uses ``multilingual`` and ignores ``languages``. - /// A non-object / empty config is ignored (leaves the mode unset), - /// mirroring Python's ``if config and isinstance(config, dict)``. - /// - /// Corresponds to ``AIConfigMixin.set_multilingual``. + /// A non-object / empty config is ignored (leaves the mode unset). AgentBase& set_multilingual(const json& config); AgentBase& add_pronunciation(const std::string& replace_val, const std::string& with_val, @@ -414,19 +434,19 @@ class AgentBase : public swml::Service { AgentBase& set_params(const json& params); AgentBase& set_global_data(const json& data); AgentBase& update_global_data(const json& data); - /// The accumulated global-data object (Python: ``AgentBase._global_data``). - /// Returns a copy; an empty object when nothing has been set. + /// The accumulated global-data object. Returns a copy; an empty object when + /// nothing has been set. [[nodiscard]] json get_global_data() const; AgentBase& set_native_functions(const std::vector& funcs); - /// The native SWAIG functions this agent declares (reference: - /// ``self.native_functions``) — rendered into the SWML ``ai.SWAIG - /// .native_functions`` array when non-empty. A caller supplies these at - /// construction or via ``set_native_functions``, so a caller reads them back. + /// The native SWAIG functions this agent declares — rendered into the SWML + /// ``ai.SWAIG.native_functions`` array when non-empty. A caller supplies + /// these at construction or via ``set_native_functions``, so a caller reads + /// them back. [[nodiscard]] const std::vector& native_functions() const { return native_functions_; } - /// This agent's id (reference: ``self.agent_id``) — the id supplied at - /// construction, or a generated UUID when none was given. + /// This agent's id — the id supplied at construction, or a generated UUID + /// when none was given. [[nodiscard]] const std::string& agent_id() const { return agent_id_; } /// The complete set of internal SWAIG function names that accept /// fillers, matching the SWAIGInternalFiller schema definition. @@ -472,7 +492,19 @@ class AgentBase : public swml::Service { /// what fillers do. Names outside the supported set log a warning. AgentBase& add_internal_filler(const std::string& function_name, const std::string& language_code, const std::vector& fillers); - AgentBase& enable_debug_events(bool enable = true); + /// Enable the debug-event webhook for this agent. + /// + /// @param level Debug event verbosity level. Defaults to 1. + /// 1 = high-level events (barge, errors, session start/end, step changes) + /// 2+ = adds high-volume events (every LLM request/response, + /// conversation_add) + /// + /// When enabled, the rendered ``ai`` verb carries + /// ``params.debug_webhook_url`` (this agent's ``/debug_events`` endpoint) + /// and ``params.debug_webhook_level`` — the two keys the SWML schema + /// defines. A verbosity LEVEL is not expressible as a bool, which is why + /// this takes an ``int``. + AgentBase& enable_debug_events(int level = 1); AgentBase& add_function_include(const json& include); AgentBase& set_function_includes(const std::vector& includes); AgentBase& set_prompt_llm_params(const json& params = json::object()); @@ -499,9 +531,8 @@ class AgentBase : public swml::Service { [[nodiscard]] bool has_contexts() const; /// Return the defined contexts as a serialised JSON object, or - /// ``std::nullopt`` when no contexts exist. Mirrors Python's - /// ``PromptMixin.contexts`` property (the read side of the contexts POM). - /// Alias of ``get_contexts`` under the Python-canonical name. + /// ``std::nullopt`` when no contexts exist — the read side of the contexts + /// POM. Alias of ``get_contexts``. [[nodiscard]] std::optional contexts() const { return get_contexts(); } /// Remove all contexts, returning the agent to a no-contexts state. @@ -521,8 +552,7 @@ class AgentBase : public swml::Service { // to the string overloads above via skills::skill_name_value(), so the // enum and the bare string load the IDENTICAL skill — the enum just adds // call-site typo checking + autocompletion. The string overloads stay the - // canonical surface (matches Python's bare str + custom skills); these - // are an idiomatic C++ addition (see PORT_ADDITIONS.md). + // canonical surface, since custom skills are named by string. AgentBase& add_skill(skills::SkillName skill_name, const json& params = json::object()); AgentBase& remove_skill(skills::SkillName skill_name); [[nodiscard]] bool has_skill(skills::SkillName skill_name) const; @@ -555,14 +585,13 @@ class AgentBase : public swml::Service { // ---- Public surface ---------------------------------------------- - /// Agent name (Python: ``get_name``). Alias of the inherited ``name()``. + /// Agent name. Alias of the inherited ``name()``. [[nodiscard]] std::string get_name() const { return name(); } - /// Override the SWAIG-webhook URL (Python: ``set_web_hook_url``). Alias of - /// set_webhook_url (the Python spelling splits ``web_hook``). + /// Override the SWAIG-webhook URL. Alias of ``set_webhook_url``. AgentBase& set_web_hook_url(const std::string& url) { return set_webhook_url(url); } - /// Add multiple SWAIG query params at once (Python: ``add_swaig_query_params``). + /// Add multiple SWAIG query params at once. AgentBase& add_swaig_query_params(const json& params) { for (auto it = params.begin(); it != params.end(); ++it) { if (it.value().is_string()) { @@ -572,8 +601,8 @@ class AgentBase : public swml::Service { return *this; } - /// Full URL for this agent's endpoint — host, port, route (Python: - /// ``get_full_url``). ``include_auth`` embeds basic-auth credentials. + /// Full URL for this agent's endpoint — host, port, route. + /// ``include_auth`` embeds basic-auth credentials. [[nodiscard]] std::string get_full_url(bool include_auth = false) const; // ======================================================================== @@ -583,8 +612,8 @@ class AgentBase : public swml::Service { AgentBase& enable_sip_routing(bool enable = true); AgentBase& register_sip_username(const std::string& username); AgentBase& auto_map_sip_usernames(bool enable = true); - /// The registered SIP usernames (lowercased set — Python: - /// ``AgentBase._sip_usernames``). Returned in registration order. + /// The registered SIP usernames (lowercased). Returned in registration + /// order. [[nodiscard]] std::vector get_sip_usernames() const; // ======================================================================== @@ -605,8 +634,8 @@ class AgentBase : public swml::Service { // Resolution order at runtime: // 1. ``set_signing_key(...)`` (explicit) // 2. ``SIGNALWIRE_SIGNING_KEY`` env var (fallback) - // When neither is set, AgentBase logs a startup warning matching the - // Python reference and accepts unsigned POSTs. + // When neither is set, AgentBase logs a startup warning and accepts + // unsigned POSTs. // ======================================================================== /// Set the SignalWire Signing Key (Dashboard → API Credentials). @@ -640,12 +669,12 @@ class AgentBase : public swml::Service { const std::map& query_params, const json& body_params, const std::map& headers) const; - /// Framework-free request-dispatch core (Python: - /// ``AgentBase.handle_request``). Overrides ``SWMLService::handle_request`` so - /// the primitive dispatch surface renders SWML via AgentBase's request-aware - /// render path (``render_swml_for_request``) instead of the base - /// ``render_document``. Over plain ``(method, url, headers, body)`` primitives - /// it performs proxy detection, basic-auth over the header map, and the + /// Framework-free request-dispatch core. Overrides + /// ``swml::Service::handle_request`` so dispatch renders SWML via AgentBase's + /// request-aware render path (``render_swml_for_request``) instead of the + /// base ``render_document``. Over plain ``(method, url, headers, body)`` + /// primitives it performs proxy detection, basic-auth over the header map, + /// and the /// routing-callback check, returning a ``(status, response_headers, /// body_string)`` triple with the 401-auth and 307-redirect behavior /// preserved. @@ -656,10 +685,9 @@ class AgentBase : public swml::Service { /// Auto-detect (or force via ``mode``) the serverless platform and dispatch /// the request to the matching handler, returning the ``(status, headers, - /// body)`` response. Canonical entry point for Python - /// ``ServerlessMixin.handle_serverless_request(event, context, mode)``: - /// ``mode`` empty = auto-detect via get_execution_mode, selecting - /// lambda / google_cloud_function / azure_function / cgi. Delegates to the + /// body)`` response. ``mode`` empty = auto-detect via get_execution_mode, + /// selecting lambda / google_cloud_function / azure_function / cgi. + /// Delegates to the /// per-platform dispatchers in ``signalwire::utils`` (handle_lambda / _gcf / /// _azure / _cgi); an unknown/``"server"`` mode renders SWML via a plain GET /// so a dispatch always produces a real response. @@ -693,14 +721,13 @@ class AgentBase : public swml::Service { // security ``__token`` on each SECURE tool's webhook (see // build_swaig_functions). NOT defaulted on purpose: a defaulted call_id // silently renders every secure tool WITHOUT its token at any call site that - // forgets to thread it, which is exactly the security regression the - // SECURE-DEFAULT gate exists to catch. Make omission a compile error. + // forgets to thread it. Make omission a compile error instead. [[nodiscard]] json build_ai_verb(const std::string& webhook_url, const std::string& call_id) const; // Build SWAIG functions array. A SECURE tool rendered with a non-empty // ``call_id`` carries a per-tool ``__token=`` on its ``web_hook_url`` — the - // wire manifestation of ``secure`` (reference agent_base.py:1040/1096-1100). + // wire manifestation of ``secure``. // ``call_id`` is not defaulted; see build_ai_verb. [[nodiscard]] json build_swaig_functions(const std::string& webhook_url, const std::string& call_id) const; @@ -721,8 +748,41 @@ class AgentBase : public swml::Service { // Handle SWAIG request void handle_swaig_request(const httplib::Request& req, httplib::Response& res); + /// The SOLE `secure` enforcement decision for one SWAIG call, deliberately + /// free of any request/transport type so that EVERY transport — the HTTP + /// `/swaig` endpoint and all four serverless envelopes (lambda, cgi, + /// google_cloud_function, azure_function) — reaches the identical check with + /// identical semantics. Each transport is responsible only for EXTRACTING the + /// credential from its own payload shape; none of them re-implements the + /// decision, so serverless cannot drift away from HTTP. + /// + /// A tool registered with `secure = true` REQUIRES a valid token. An ABSENT + /// token is refused exactly like a forged one — omitting the credential must + /// never be weaker than presenting a wrong one, or `secure` would be a flag + /// that permits anonymous calls. A token can only be checked against a + /// `call_id`, so an absent `call_id` counts as UNVALIDATED, never a bypass. + /// A tool with `secure = false` is never refused. + /// + /// @param function_name The SWAIG function being invoked. + /// @param token The credential from the caller's query string, or + /// `std::nullopt` when absent. + /// @param call_id The call the token must be bound to, or `std::nullopt` + /// when absent. + /// @return `std::nullopt` to proceed with dispatch, or the refusal to return + /// INSTEAD of dispatching. The refusal is always delivered as a + /// 200 + FunctionResult body, never an HTTP error status: the engine + /// has no handling for a refusal status, so a non-200 would be + /// dropped rather than relayed to the caller. + [[nodiscard]] std::optional swaig_validate_token( + const std::string& function_name, const std::optional& token, + const std::optional& call_id) const; + // Handle post_prompt request void handle_post_prompt_request(const httplib::Request& req, httplib::Response& res); + /// Receives the AI module's debug-event webhook POSTs. Mounted only when + /// ``enable_debug_events()`` has been called, which is also what puts + /// ``params.debug_webhook_url`` on the wire. + void handle_debug_events_request(const httplib::Request& req, httplib::Response& res); // Validate basic auth bool validate_auth(const httplib::Request& req, httplib::Response& res) const; @@ -731,9 +791,8 @@ class AgentBase : public swml::Service { static void add_security_headers(httplib::Response& res); // Internal SWML rendering (used by render_swml_for_request). ``call_id`` is - // the request's ``call_id`` query parameter (reference - // swml_service.py:807 → ``_render_swml(call_id)``); when non-empty every - // SECURE tool's rendered webhook carries its per-tool ``__token``. + // the request's ``call_id`` query parameter; when non-empty every SECURE + // tool's rendered webhook carries its per-tool ``__token``. [[nodiscard]] json render_swml_internal(const std::map& headers, const std::string& call_id) const; @@ -774,7 +833,9 @@ class AgentBase : public swml::Service { json global_data_; std::vector native_functions_; json internal_fillers_; - bool debug_events_ = false; + bool debug_events_enabled_ = false; + /// Debug-event verbosity level; defaults to 1. + int debug_events_level_ = 1; json prompt_llm_params_; json post_prompt_llm_params_; @@ -813,55 +874,49 @@ class AgentBase : public swml::Service { DebugEventCallback debug_event_callback_; // ======================================================================== - // Construction-parameter accessors for the reference's UNDERSCORE-PRIVATE - // attributes. Protected (not public) because the reference's counterparts - // are private — subclasses and the render pipeline read them, callers do - // not. + // Construction-parameter accessors. Protected rather than public — + // subclasses and the render pipeline read them, callers do not. // ======================================================================== - // NOTE: ``agent_id()`` / ``token_expiry_secs()`` are declared PUBLIC (above), - // not here — they read configuration that is part of the public API, so - // callers may query them directly. + // NOTE: ``agent_id()`` is declared PUBLIC (above), not here — it reads + // configuration that is part of the public API, so callers may query it + // directly. - /// reference: ``self._auto_answer`` — gates the PHASE-2 ``answer`` verb. + /// Gates the PHASE-2 ``answer`` verb. [[nodiscard]] bool auto_answer() const { return auto_answer_; } - /// reference: ``self._record_call`` / ``_record_format`` / ``_record_stereo``. + /// Call-recording configuration; see the ``record_call_`` fields below. [[nodiscard]] bool record_call_enabled() const { return record_call_; } [[nodiscard]] const std::string& record_format() const { return record_format_; } [[nodiscard]] bool record_stereo() const { return record_stereo_; } - /// reference: ``self._default_webhook_url``. + /// The SWAIG default ``web_hook_url``, when one was supplied. [[nodiscard]] const std::optional& default_webhook_url() const { return default_webhook_url_; } - /// reference: ``self._suppress_logs``. + /// Whether agent logging is suppressed. [[nodiscard]] bool suppress_logs() const { return suppress_logs_; } - /// Accepted and stored by the reference constructor with no consumer. + /// Accepted and stored at construction; no consumer on the render path. [[nodiscard]] bool enable_post_prompt_override() const { return enable_post_prompt_override_; } [[nodiscard]] bool check_for_input_override() const { return check_for_input_override_; } - /// Token lifetime forwarded to this agent's ``SessionManager``. PROTECTED, - /// unlike ``agent_id()``: the reference's AgentBase does NOT keep a - /// ``self.token_expiry_secs`` — it forwards the ctor param straight into - /// ``SessionManager(token_expiry_secs=…)``, where it IS public - /// (``SessionManager::token_expiry_secs()``). A public accessor here would be - /// surface the reference's AgentBase does not have. + /// Token lifetime forwarded to this agent's ``SessionManager``, which is + /// where it is publicly readable (``SessionManager::token_expiry_secs()``). + /// AgentBase does not keep its own copy, so this is a protected convenience + /// rather than public surface. [[nodiscard]] int token_expiry_secs() const { return session_manager_.token_expiry_secs(); } - // Construction parameters the reference stores on the instance. - /// ``self.agent_id`` — the supplied id, or a generated UUID. + // Construction parameters stored on the instance. + /// The supplied agent id, or a generated UUID. std::string agent_id_; - /// ``self._auto_answer`` — gates the PHASE-2 ``answer`` verb. + /// Gates the PHASE-2 ``answer`` verb. bool auto_answer_ = true; - /// ``self._record_call`` / ``_record_format`` / ``_record_stereo`` — gate - /// and shape the PHASE-3 ``record_call`` verb. + /// Gate and shape the PHASE-3 ``record_call`` verb. bool record_call_ = false; std::string record_format_ = "mp4"; bool record_stereo_ = true; - /// ``self._default_webhook_url`` — SWAIG default ``web_hook_url``. + /// The SWAIG default ``web_hook_url``. std::optional default_webhook_url_; - /// ``self._suppress_logs``. + /// Whether agent logging is suppressed. bool suppress_logs_ = false; - /// Accepted and stored by the reference constructor; no render-path - /// consumer in the reference either. + /// Accepted and stored at construction; no render-path consumer. bool enable_post_prompt_override_ = false; bool check_for_input_override_ = false; diff --git a/include/signalwire/agents/bedrock.hpp b/include/signalwire/agents/bedrock.hpp index d3893c0..40aadcf 100644 --- a/include/signalwire/agents/bedrock.hpp +++ b/include/signalwire/agents/bedrock.hpp @@ -19,8 +19,6 @@ using json = nlohmann::json; /// transformed into ``amazon_bedrock`` and voice/inference params are folded /// into the prompt object (Bedrock carries voice + inference inside ``prompt``, /// not as sibling fields). -/// -/// Corresponds to ``signalwire.agents.bedrock.BedrockAgent``. class BedrockAgent : public agent::AgentBase { public: explicit BedrockAgent(const std::string& name = "bedrock_agent", @@ -32,8 +30,8 @@ class BedrockAgent : public agent::AgentBase { /// Set the Bedrock voice ID (e.g. "matthew", "joanna"). void set_voice(const std::string& voice_id); - /// Update Bedrock inference params. A negative value leaves that param - /// unchanged (mirrors Python's ``None`` = "don't update"). + /// Update Bedrock inference params. A negative value means "don't update" and + /// leaves that param unchanged. void set_inference_params(double temperature = -1.0, double top_p = -1.0, int max_tokens = -1); /// Not applicable for Bedrock (fixed voice-to-voice model) — logs a warning. @@ -49,7 +47,7 @@ class BedrockAgent : public agent::AgentBase { /// Not applicable for Bedrock — use set_inference_params() instead; logs a warning. void set_prompt_llm_params(const json& params = json::object()); - /// String representation (Python: ``__repr__``). + /// String representation of this agent, for debugging and logging. [[nodiscard]] std::string repr() const; protected: diff --git a/include/signalwire/ai_chat/ai_chat_client.hpp b/include/signalwire/ai_chat/ai_chat_client.hpp index 9ca4033..c89aa51 100644 --- a/include/signalwire/ai_chat/ai_chat_client.hpp +++ b/include/signalwire/ai_chat/ai_chat_client.hpp @@ -16,10 +16,9 @@ using json = nlohmann::json; // ── Errors ─────────────────────────────────────────────────────────── // -// Typed error family for the SignalWire AI Chat service, mirroring the python -// reference (signalwire.ai_chat.client). Callers catch the one base type -// (``AIChatError``) for every AI-Chat failure and can branch on ``code()`` or -// the concrete subclass. +// Typed error family for the SignalWire AI Chat service. Callers catch the one +// base type (``AIChatError``) for every AI-Chat failure and can branch on +// ``code()`` or the concrete subclass. // // Success/failure is decided by the JSON-RPC BODY, not the HTTP status (the // service's keepalive heartbeat commits 200 before the turn's outcome is @@ -28,8 +27,8 @@ using json = nlohmann::json; /// Base error for AI Chat service failures. Carries the JSON-RPC error /// ``code`` (or a sentinel when the failure rode the SUCCESS envelope, as with /// ``SummaryError``) and the server ``message``. ``has_code()`` distinguishes a -/// real JSON-RPC code from the no-code (success-envelope) case; python models -/// that as ``code=None``, which C++ renders as ``has_code()==false``. +/// real JSON-RPC code from the no-code (success-envelope) case, which renders +/// as ``has_code()==false``. class AIChatError : public std::runtime_error { public: AIChatError(int code, const std::string& message) @@ -46,7 +45,7 @@ class AIChatError : public std::runtime_error { /// The JSON-RPC error code. Only meaningful when ``has_code()`` is true. int code() const { return code_; } /// Whether a real JSON-RPC error code is carried (false == success-envelope - /// failure, python's ``code is None``). + /// failure, which has no code). bool has_code() const { return has_code_; } /// The server-provided error message (without the ``[code]`` prefix). const std::string& server_message() const { return message_; } @@ -99,7 +98,8 @@ struct ConversationInfo { std::string id; /// Lifecycle status the service reported (e.g. ``"created"``). std::string status; - /// Whether an opening assistant message was produced (python ``None``). + /// Whether an opening assistant message was produced; false when the service + /// returned none. bool has_initial_message = false; /// The opening assistant message, when ``has_initial_message`` is true. std::string initial_message; @@ -126,14 +126,13 @@ struct ChatLog { // ── Options ────────────────────────────────────────────────────────── /// Per-turn options common to ``create_conversation`` and ``chat``. Unset -/// (empty / zero / false) fields are omitted from the wire params entirely, -/// matching the python reference's truthiness guards. +/// (empty / zero / false) fields are omitted from the wire params entirely. struct ConversationTurnOptions { /// Config URL locating the agent config (required on create; auto-creates on /// chat when present). std::string config_url; /// Conversation inactivity timeout in seconds (wire ``conversation_timeout``). - /// 0 == unset (omitted), mirroring python's ``if timeout``. + /// 0 == unset, and is omitted from the wire params. int timeout = 0; /// Reinitialize an existing conversation. bool reinit = false; @@ -153,8 +152,8 @@ struct ChatOptions : ConversationTurnOptions { }; /// Sampling / prompt options for ``summarize``. Every numeric field is optional -/// (its ``has_*`` flag gates whether it is sent), matching python's -/// ``**sampling`` filtered by ``v is not None``. +/// (its ``has_*`` flag gates whether it is sent); unset fields are left out of +/// the wire params. struct SummarizeOptions { /// Custom prompt steering the summary (wire ``summary_prompt``). std::string summary_prompt; @@ -182,9 +181,9 @@ struct AIChatClientOptions { /// Fully-qualified endpoint URL, used verbatim (highest precedence). std::string url; /// Idle read timeout in seconds (byte-silence, NOT total turn length). - /// Mirrors the python reference's ``sock_read=60``. 0 disables it. + /// Defaults to 60; 0 disables it. int read_idle_timeout_seconds = 60; - /// Bounded connect timeout in seconds (python ``connect=10``). + /// Bounded connect timeout in seconds. Defaults to 10. int connect_timeout_seconds = 10; }; @@ -203,11 +202,9 @@ struct AIChatClientOptions { /// byte-driven, not wall-clock: there is no total-request cap an idle-but-live /// turn could trip. cpp-httplib's ``set_read_timeout`` is a PER-READ (per /// socket recv) idle timeout — each keepalive whitespace read resets it — so it -/// is exactly the ``sock_read=60`` semantics of the python reference, rather -/// than a total transfer cap a heartbeat can't reset. Leading keepalive -/// whitespace is valid JSON, so the buffered parse is unaffected. -/// -/// Mirrors the python reference ``signalwire.ai_chat.AIChatClient``. +/// bounds byte-silence rather than acting as a total transfer cap a heartbeat +/// can't reset. Leading keepalive whitespace is valid JSON, so the buffered +/// parse is unaffected. class AIChatClient { public: /// @throws std::invalid_argument when no project resolves, or no URL can be @@ -223,10 +220,8 @@ class AIChatClient { /// Release any client-owned transport resources. cpp-httplib is stateless (a /// fresh ``httplib::Client`` is created per request), so there is no persistent - /// session to tear down -- this is a well-defined no-op that keeps the - /// python reference's explicit-release / context-manager-exit shape - /// (``client.close()`` / ``async with client:``) usable verbatim. Mirrors the - /// python reference ``AIChatClient.close`` (and the TS no-op ``close()``). + /// session to tear down -- this is a well-defined no-op, provided so callers + /// can write an explicit-release teardown without special-casing this client. /// Idempotent; safe to call more than once. The destructor needs no extra work. void close(); diff --git a/include/signalwire/contexts/contexts.hpp b/include/signalwire/contexts/contexts.hpp index fc349f4..439b5ae 100644 --- a/include/signalwire/contexts/contexts.hpp +++ b/include/signalwire/contexts/contexts.hpp @@ -11,6 +11,16 @@ #include namespace signalwire { + +// Forward declaration for the friendship below: AgentBase is the only intended +// caller of ContextBuilder's private attach_tool_name_supplier() wiring seam. +// Declared rather than #included to keep contexts.hpp free of an agent/ include +// (agent_base.hpp already includes this header, so including it back would be +// circular). +namespace agent { +class AgentBase; +} + namespace contexts { using json = nlohmann::json; @@ -32,34 +42,46 @@ constexpr int MAX_STEPS_PER_CONTEXT = 100; // GatherQuestion // ============================================================================ +/// One question in a step's ``gather_info`` questionnaire. +/// +/// Each question names the ``key`` its answer is stored under, the +/// ``question`` text put to the caller, and the answer ``type``. Optional +/// per-question overrides narrow the behaviour for this question only: +/// ``confirm`` reads the answer back for confirmation, ``prompt`` replaces the +/// gather-level prompt, ``functions`` limits which SWAIG tools are callable +/// while it is being answered, and ``isolated`` is tri-state — unset inherits +/// the ``GatherInfo`` default, a set value overrides it. +/// +/// Immutable after construction; ``to_json`` emits only the fields that differ +/// from their default, so a bare (key, question) pair renders as a minimal +/// object. class GatherQuestion { public: GatherQuestion(const std::string& key, const std::string& question, const std::string& type = "string", bool confirm = false, - const std::string& prompt = "", const std::vector& functions = {}, + const std::optional& prompt = std::nullopt, + const std::vector& functions = {}, const std::optional& isolated = std::nullopt); [[nodiscard]] json to_json() const; - // Every construction parameter the reference keeps as a public instance - // attribute (`self.key`, `self.question`, …) is readable back here. + // Every construction parameter is readable back here. [[nodiscard]] const std::string& key() const { return key_; } - /// reference: ``self.question`` — the question text put to the caller. + /// The question text put to the caller. [[nodiscard]] const std::string& question() const { return question_; } - /// reference: ``self.type`` — answer type ("string" by default); emitted to - /// SWML only when it differs from the default. + /// Answer type ("string" by default); emitted to SWML only when it differs + /// from the default. [[nodiscard]] const std::string& type() const { return type_; } - /// reference: ``self.confirm`` — whether the answer is read back for - /// confirmation; emitted only when true. + /// Whether the answer is read back for confirmation; emitted only when true. [[nodiscard]] bool confirm() const { return confirm_; } - /// reference: ``self.prompt`` — per-question prompt override; emitted only - /// when non-empty. - [[nodiscard]] const std::string& prompt() const { return prompt_; } - /// reference: ``self.functions`` — SWAIG functions available while this - /// question is being answered; emitted only when non-empty. + /// Per-question prompt override; ``nullopt`` when absent, and emitted only + /// when set to a non-empty string. + [[nodiscard]] const std::optional& prompt() const { return prompt_; } + /// SWAIG functions available while this question is being answered; emitted + /// only when non-empty. [[nodiscard]] const std::vector& functions() const { return functions_; } - /// reference: ``self.isolated`` — tri-state; ``nullopt`` inherits the - /// gather_info default, a set value overrides it (emitted even when false). + /// Tri-state; ``nullopt`` inherits the gather_info default, a set value + /// overrides it (emitted even when false). [[nodiscard]] const std::optional& isolated() const { return isolated_; } private: @@ -67,7 +89,7 @@ class GatherQuestion { std::string question_; std::string type_; bool confirm_; - std::string prompt_; + std::optional prompt_; std::vector functions_; // Tri-state: nullopt means "inherit the gather_info default". std::optional isolated_; @@ -77,14 +99,27 @@ class GatherQuestion { // GatherInfo // ============================================================================ +/// A step's structured data-collection block — the ``gather_info`` object. +/// +/// Attaching one to a ``Step`` turns that step into a questionnaire: the +/// runtime injects the reserved ``gather_submit`` tool (see +/// ``reserved_native_tool_names``) and walks the caller through each +/// ``GatherQuestion`` in order, writing the collected answers under +/// ``output_key``. ``completion_action`` says what happens once every question +/// is answered, ``prompt`` supplies the gather-wide prompt each question may +/// override, and ``isolated`` is the default the questions inherit when they +/// do not set their own. +/// +/// ``add_question`` appends and returns ``*this`` for fluent chaining. class GatherInfo { public: - GatherInfo(const std::string& output_key = "", const std::string& completion_action = "", - const std::string& prompt = "", bool isolated = false); + GatherInfo(const std::optional& output_key = std::nullopt, + const std::optional& completion_action = std::nullopt, + const std::optional& prompt = std::nullopt, bool isolated = false); GatherInfo& add_question(const std::string& key, const std::string& question, const std::string& type = "string", bool confirm = false, - const std::string& prompt = "", + const std::optional& prompt = std::nullopt, const std::vector& functions = {}, const std::optional& isolated = std::nullopt); @@ -92,13 +127,15 @@ class GatherInfo { [[nodiscard]] bool has_questions() const { return !questions_.empty(); } [[nodiscard]] const std::vector& questions() const { return questions_; } - [[nodiscard]] const std::string& completion_action() const { return completion_action_; } + [[nodiscard]] const std::optional& completion_action() const { + return completion_action_; + } private: std::vector questions_; - std::string output_key_; - std::string completion_action_; - std::string prompt_; + std::optional output_key_; + std::optional completion_action_; + std::optional prompt_; bool isolated_ = false; }; @@ -106,6 +143,26 @@ class GatherInfo { // Step // ============================================================================ +/// One stage of a context's guided flow — the unit the runtime advances +/// through. +/// +/// A step carries its own prompt (raw text via ``set_text``, or POM sections +/// via ``add_section``/``add_bullets``) and the rules for leaving it: +/// ``set_step_criteria`` states when it is complete, ``set_valid_steps`` / +/// ``set_valid_contexts`` declare where the model may navigate next (which is +/// what causes the reserved ``next_step`` / ``change_context`` tools to be +/// injected), and ``set_end`` marks it terminal for the flow. +/// +/// Three behaviours are easy to get wrong and are documented on their setters: +/// * ``set_functions`` — the active tool set is INHERITED from the previous +/// step unless a step declares its own; +/// * ``set_end(true)`` — exits step mode, it does NOT end the call; +/// * ``set_gather_info`` — while a gather is running, every other tool +/// except ``gather_submit`` and the question's own ``functions`` is +/// deactivated, including the navigation tools. +/// +/// All mutators return ``*this`` for fluent chaining; ``to_json`` emits the +/// step object embedded in the SWML contexts structure. class Step { public: Step() = default; @@ -206,8 +263,9 @@ class Step { /// must ask rather than derive the answer from an earlier one. A /// question's own isolated overrides this. The hidden turns remain in /// the call log. - Step& set_gather_info(const std::string& output_key = "", - const std::string& completion_action = "", const std::string& prompt = "", + Step& set_gather_info(const std::optional& output_key = std::nullopt, + const std::optional& completion_action = std::nullopt, + const std::optional& prompt = std::nullopt, bool isolated = false); /// Add a gather question (set_gather_info must be called first). @@ -236,7 +294,7 @@ class Step { /// inherits the gather's setting. Step& add_gather_question(const std::string& key, const std::string& question, const std::string& type = "string", bool confirm = false, - const std::string& prompt = "", + const std::optional& prompt = std::nullopt, const std::vector& functions = {}, const std::optional& isolated = std::nullopt); @@ -294,6 +352,28 @@ class Step { // Context // ============================================================================ +/// A named, ordered collection of ``Step``s — one mode of an agent's workflow. +/// +/// A ``ContextBuilder`` owns one or more contexts and exactly one is active at +/// a time; ``set_valid_contexts`` declares which others the model may switch +/// to via the reserved ``change_context`` tool. Entering a context begins at +/// its first step unless ``set_initial_step`` names another — useful when +/// re-entry should skip a preamble. +/// +/// Beyond its steps, a context carries prompt material applied for as long as +/// it is active: ``set_prompt``/``add_section``/``add_bullets`` for the +/// context prompt, ``set_system_prompt``/``add_system_section``/ +/// ``add_system_bullets`` for the system prompt, ``set_post_prompt`` to +/// override the agent's summary prompt, and enter/exit fillers spoken across +/// the switch. Conversation visibility is controlled by ``set_history`` (the +/// default each step's own ``set_history`` overrides) and by +/// ``set_isolated`` — noting that a reset configuration +/// (``set_consolidate`` / ``set_full_reset``) takes precedence over the +/// isolated wipe. +/// +/// Steps are keyed by name and kept in insertion order; ``add_step`` returns a +/// reference to the step for chaining, and ``move_step``/``remove_step`` +/// rearrange that order. All context mutators return ``*this``. class Context { public: Context() = default; @@ -484,12 +564,6 @@ class ContextBuilder { /// Get an existing context [[nodiscard]] Context* get_context(const std::string& name); - /// Attach a tool-name supplier so validate() can check - /// user-defined SWAIG tool names against - /// reserved_native_tool_names(). AgentBase::define_contexts() - /// wires this up automatically. - ContextBuilder& attach_tool_name_supplier(std::function()> supplier); - /// Validate all contexts. Checks: /// - At least one context is defined /// - A single context must be named "default" @@ -505,6 +579,24 @@ class ContextBuilder { [[nodiscard]] bool has_contexts() const { return !contexts_.empty(); } private: + /// INTERNAL WIRING SEAM — deliberately NOT public API. + /// + /// Attaches a tool-name supplier so validate() can check user-defined SWAIG + /// tool names against reserved_native_tool_names(). + /// + /// The Python reference has no equivalent method: there, the agent reaches its + /// own tool registry directly. C++ cannot, so AgentBase hands the builder a + /// closure over list_tools() (agent_base.cpp:897) — an implementation detail + /// of how this port wires the two together, not a capability a caller is meant + /// to reach for. It was public by accident, which put an invented method on + /// the audited surface and, because its std::function parameter has no + /// vocabulary type, silently dropped the symbol from port_signatures.json. + /// Made private 2026-07-30; behaviour is unchanged. + ContextBuilder& attach_tool_name_supplier(std::function()> supplier); + + /// AgentBase::define_contexts() is the ONLY intended caller of the seam above. + friend class ::signalwire::agent::AgentBase; + std::map contexts_; std::vector context_order_; std::function()> tool_name_supplier_; diff --git a/include/signalwire/core/auth_handler.hpp b/include/signalwire/core/auth_handler.hpp index 0fceef2..8e95aeb 100644 --- a/include/signalwire/core/auth_handler.hpp +++ b/include/signalwire/core/auth_handler.hpp @@ -3,19 +3,14 @@ // // Unified authentication handler supporting multiple auth methods. // -// C++ port of the Python reference -// ``signalwire.core.auth_handler.AuthHandler`` (cross-checked against the Java -// ``com.signalwire.sdk.core.AuthHandler``). Provides a clean pattern for Basic -// Auth, Bearer tokens, and API keys across all SignalWire services. All -// credential comparisons are timing-safe. +// Provides a clean pattern for Basic Auth, Bearer tokens, and API keys across +// all SignalWire services. All credential comparisons are timing-safe. // -// Idiom note: Python's ``flask_decorator`` / ``get_fastapi_dependency`` are -// framework-bound (Flask / FastAPI). C++ ships neither web framework, so the -// native equivalents here are framework-neutral, modelled exactly like the Java -// port: a "request" is a case-insensitive header map (``Headers``). These two -// methods EXIST (so the surface has the names) and are REAL — they enforce/ -// report authentication over a header map — they just don't bind to a specific -// C++ web framework. +// ``flask_decorator`` and ``get_fastapi_dependency`` are named after the +// framework-integration shapes they serve, but they do NOT bind to any web +// framework. A "request" is simply a case-insensitive header map (``Headers``), +// so either one drops into whatever HTTP layer the caller is using. Both are +// real enforcement, not stubs. #pragma once #include @@ -42,8 +37,10 @@ struct BasicCredentials { }; /// Bearer-token credential carrier (matches FastAPI's -/// HTTPAuthorizationCredentials). +/// HTTPAuthorizationCredentials, which carries BOTH halves of the +/// ``Authorization`` header: the scheme token and the credential string). struct BearerCredentials { + std::string scheme; std::string credentials; }; @@ -72,6 +69,36 @@ class AuthException : public std::runtime_error { AuthResponse response_; }; +/// Unified authentication over a request header map — HTTP Basic, Bearer +/// token, and API key. +/// +/// The handler is constructed from a ``SecurityConfig`` and authenticates a +/// request by trying the ENABLED methods in order — Bearer, then API key, then +/// Basic — reporting which one succeeded. **Basic is always on**, seeded from +/// ``SecurityConfig::get_basic_auth``, which generates a random password when +/// none is configured, so the endpoint is never credential-free. Bearer and API +/// key are **off**, because ``SecurityConfig`` carries no token or key field. +/// Their verifiers are real and correct — a subclass that supplies a token or +/// key gets working enforcement — but out of the box a Bearer or ``X-API-Key`` +/// header authenticates nothing. +/// +/// **Every credential comparison is timing-safe** (constant-time over the full +/// operand), so a caller cannot recover a secret byte-by-byte from response +/// latency. Header lookup is case-insensitive, matching HTTP; the API-key +/// header name defaults to ``X-API-Key`` and comes from the config. A failed +/// attempt is logged without the submitted credential, and ``get_auth_info`` +/// reports which methods are enabled but never a secret. The API-key header +/// name is ``X-API-Key``. +/// +/// Two entry points wrap that core, both framework-neutral over ``Headers`` +/// and both real enforcement: ``get_fastapi_dependency`` returns a +/// callable yielding an ``AuthResult`` — throwing ``AuthException`` when +/// ``optional`` is false and auth fails — and ``flask_decorator`` wraps a +/// downstream ``RequestHandler`` so unauthenticated requests get an HTTP 401 +/// with a ``WWW-Authenticate`` challenge and never reach it. +/// +/// Holds a REFERENCE to the ``SecurityConfig``: the config must outlive the +/// handler. class AuthHandler { public: /// Framework-neutral request handler: header map in, ``AuthResponse`` out. @@ -83,9 +110,9 @@ class AuthHandler { /// Initialize the auth handler with a ``SecurityConfig``. explicit AuthHandler(SecurityConfig& security_config); - /// The ``SecurityConfig`` this handler authenticates against (reference: - /// ``self.security_config``). The caller constructs the handler with it, so - /// the caller can read it back — e.g. to inspect which auth modes are on. + /// The ``SecurityConfig`` this handler authenticates against. The caller + /// constructs the handler with it, so the caller can read it back — e.g. to + /// inspect which auth modes are on. [[nodiscard]] const SecurityConfig& security_config() const { return security_config_; } /// Verify basic-auth credentials. Timing-safe. @@ -97,13 +124,13 @@ class AuthHandler { /// Verify an API key. Timing-safe. [[nodiscard]] bool verify_api_key(const std::string& api_key) const; - /// Framework-neutral equivalent of Python's FastAPI dependency. Returns a + /// Framework-neutral auth dependency. Returns a /// callable taking a request header map and returning an ``AuthResult``. /// When ``optional`` is false and authentication fails, the callable throws /// ``AuthException``; when true it returns the (unauthenticated) result. [[nodiscard]] FastapiDependency get_fastapi_dependency(bool optional = false) const; - /// Framework-neutral equivalent of Python's Flask decorator. Given a + /// Framework-neutral auth decorator. Given a /// downstream ``RequestHandler``, returns a wrapping handler that enforces /// authentication: authenticated requests pass through, others get an HTTP /// 401 with a WWW-Authenticate challenge. @@ -127,7 +154,7 @@ class AuthHandler { SecurityConfig& security_config_; - // Configured auth methods (matches Python's self.auth_methods dict). + // Configured auth methods. bool basic_enabled_ = false; std::string basic_username_; std::string basic_password_; diff --git a/include/signalwire/core/config_loader.hpp b/include/signalwire/core/config_loader.hpp index 91d5966..c791b7d 100644 --- a/include/signalwire/core/config_loader.hpp +++ b/include/signalwire/core/config_loader.hpp @@ -3,18 +3,14 @@ // // Configuration loader with environment-variable substitution. // -// C++ port of the Python reference -// ``signalwire.core.config_loader.ConfigLoader`` (cross-checked against the -// Java ``com.signalwire.sdk.core.ConfigLoader``). Supports ``${VAR|default}`` -// syntax for referencing environment variables within configuration files. The -// first existing, parseable file in the search paths wins. +// Supports ``${VAR|default}`` syntax for referencing environment variables +// within configuration files. The first existing, parseable file in the search +// paths wins. // -// Idiom mapping: the C++ port parses JSON only — the vendored ``nlohmann::json`` -// is a JSON library and the port carries no YAML dependency, so ``.yaml``/ -// ``.yml`` files are NOT supported here (the Python/Java ports also default to -// JSON config files; the default search paths are all ``*.json``). After -// substitution, string values that look like booleans/integers/floats are -// coerced to those native JSON types. +// JSON only: the vendored ``nlohmann::json`` is a JSON library and there is no +// YAML dependency, so ``.yaml``/``.yml`` files are NOT supported (the default +// search paths are all ``*.json``). After substitution, string values that look +// like booleans/integers/floats are coerced to those native JSON types. #pragma once #include @@ -27,6 +23,33 @@ namespace core { using json = nlohmann::json; +/// Loads a JSON configuration file and resolves ``${VAR|default}`` +/// environment-variable references inside it. +/// +/// Construction walks the supplied search paths (or the built-in defaults) and +/// keeps the FIRST file that exists and parses; ``has_config`` and +/// ``get_config_file`` report whether and which. The stored config is the RAW +/// document — substitution happens on read, so ``get``/``get_section``/ +/// ``merge_with_env`` see current environment values, while ``get_config`` +/// hands back the unsubstituted original. +/// +/// Substitution is recursive over objects and arrays. ``${VAR}`` expands to +/// the environment value, ``${VAR|default}`` falls back to ``default`` when +/// the variable is unset. Nesting deeper than ``max_depth`` (10 by default) +/// throws ``std::invalid_argument`` rather than looping. After substitution, a +/// string that looks like a boolean, integer, or float is COERCED to that +/// native JSON type, so ``"${PORT|8080}"`` reads back as a number. +/// +/// ``get`` addresses values by dot-notation path (``"security.ssl_enabled"``) +/// and returns ``default_value`` for a missing path — no exception. +/// ``merge_with_env`` folds ``SWML_``-prefixed environment variables into the +/// config (prefix stripped, lowercased, split on underscore boundaries) but +/// only where the config does not already define the key: **the config file +/// wins over the environment**, the opposite precedence from ``SecurityConfig``. +/// +/// JSON only. The vendored ``nlohmann::json`` is a JSON library and there is no +/// YAML dependency, so ``.yaml``/``.yml`` files are not supported (every +/// default search path is a ``*.json``). class ConfigLoader { public: /// Initialize the config loader. @@ -35,10 +58,10 @@ class ConfigLoader { /// parseable file wins. explicit ConfigLoader(const std::optional>& config_paths = std::nullopt); - /// The config file paths this loader searches, in order (reference: - /// ``self.config_paths``) — the caller-supplied list, or the default search - /// paths when none was given. A caller hands these in, so a caller can read - /// back exactly which paths were consulted. + /// The config file paths this loader searches, in order — the + /// caller-supplied list, or the default search paths when none was given. A + /// caller hands these in, so a caller can read back exactly which paths were + /// consulted. [[nodiscard]] const std::vector& config_paths() const { return config_paths_; } /// Check if a configuration was loaded. diff --git a/include/signalwire/core/logging_config.hpp b/include/signalwire/core/logging_config.hpp index aa0ea12..7a626e6 100644 --- a/include/signalwire/core/logging_config.hpp +++ b/include/signalwire/core/logging_config.hpp @@ -5,17 +5,21 @@ #pragma once +#include #include +// `get_logger` below returns a NAMED logger by value, so the type must be +// complete here (not merely forward-declared). +#include "signalwire/logging/logger.hpp" + namespace signalwire { namespace core { namespace logging_config { /** - * Cross-language SDK contract for serverless / deployment-mode detection. + * Detect the serverless / deployment mode from the environment. * - * Mirrors `signalwire.core.logging_config.get_execution_mode` in the - * Python reference. Order of precedence (FIRST match wins): + * Order of precedence (FIRST match wins): * * 1. GATEWAY_INTERFACE -> "cgi" * 2. AWS_LAMBDA_FUNCTION_NAME or LAMBDA_TASK_ROOT -> "lambda" @@ -32,8 +36,7 @@ std::string get_execution_mode(); /** * Configure the SDK logging system once, globally, from environment - * variables (idempotent). Mirrors Python's - * ``signalwire.core.logging_config.configure_logging``. Reads + * variables (idempotent). Reads * ``SIGNALWIRE_LOG_MODE`` (off/stderr/default/auto) and * ``SIGNALWIRE_LOG_LEVEL`` and applies them to the process logger. Safe to * call repeatedly; only the first call takes effect until @@ -43,30 +46,49 @@ void configure_logging(); /** * Reset the one-shot logging-configured flag so a subsequent - * ``configure_logging`` call re-reads the environment. Mirrors Python's - * ``reset_logging_configuration`` (useful when env vars change at runtime). + * ``configure_logging`` call re-reads the environment. Useful when env vars + * change at runtime. */ void reset_logging_configuration(); /** - * Return whether ``configure_logging`` has already run (the internal flag). - * Ensures the logger is configured on first access, mirroring Python's - * ``get_logger`` single-entry-point behavior. The C++ logger is a process - * singleton (see ``signalwire::get_logger``); this helper guarantees it has - * been configured before use and returns the configured state. + * Obtain the SDK logger, configuring it on first access. This is the single + * entry point every SDK module should use. + * + * Returns a NAMED logger BY VALUE, so the caller can log directly and ``name`` + * is honoured. Delegates to ``signalwire::logging::get_logger(name)``; the only + * thing this entry point adds is the guarantee that ``configure_logging`` has + * run first. + * + * (Note ``signalwire::get_logger()``, taking no argument, is a DIFFERENT + * overload returning the process singleton by reference; it is unrelated to + * this contract.) + * + * @param name Logical logger name, conventionally the calling module's name. + */ +::signalwire::logging::Logger get_logger(const std::string& name); + +/** + * Strip control characters from a single string. + * + * Removes ASCII control chars except ``\t``, ``\n`` and ``\r``. * - * @param name Logical logger name (recorded for API compatibility; the C++ - * Logger is a process singleton so the name is advisory). + * INTERNAL helper: the public entry point is the event-map form + * (``strip_control_chars`` below); this is the per-value scrub that form is + * built out of. */ -bool get_logger(const std::string& name); +std::string strip_control_chars_str(const std::string& value); /** - * Strip control characters (to prevent log injection) from ``value``. - * Mirrors Python's ``strip_control_chars`` structlog processor, reduced to - * the value-sanitizing core: removes ASCII control chars except ``\t``, - * ``\n`` and ``\r``. + * Strip control characters from log event values to prevent log injection. + * + * Takes the log event map, scrubs every STRING value, and returns the map. + * Non-string values pass through untouched. + * + * This is called from ``signalwire::logging::Logger::log``, so the scrub sits + * on the real emission path rather than merely being available to callers. */ -std::string strip_control_chars(const std::string& value); +nlohmann::json strip_control_chars(const nlohmann::json& event_dict); } // namespace logging_config } // namespace core diff --git a/include/signalwire/core/pom_builder.hpp b/include/signalwire/core/pom_builder.hpp index d12eede..4651d95 100644 --- a/include/signalwire/core/pom_builder.hpp +++ b/include/signalwire/core/pom_builder.hpp @@ -3,20 +3,16 @@ // // PomBuilder — standalone builder for structured POM prompts. // -// C++ port of the Python reference -// ``signalwire.core.pom_builder.PomBuilder`` (cross-checked against the Java -// ``com.signalwire.sdk.core.PomBuilder``). A flexible wrapper around the -// existing ``signalwire::pom::PromptObjectModel`` (see -// ``include/signalwire/pom/pom.hpp``) that allows dynamic creation of sections +// A flexible wrapper around the existing ``signalwire::pom::PromptObjectModel`` +// (see ``include/signalwire/pom/pom.hpp``) that allows dynamic creation of sections // on demand, adding content to existing sections, nesting subsections, and // rendering to Markdown or XML. There are no predefined section types. All // mutator methods return ``*this`` for fluent chaining. // // Section lookup: rather than caching ``Section*`` (which vector growth would // invalidate), sections are resolved by title through -// ``PromptObjectModel::find_section`` on each access — that recursive search -// resolves top-level sections, matching the Python ``_sections`` map for the -// operations this builder performs (all keyed by top-level section title). +// ``PromptObjectModel::find_section`` on each access. Every operation this +// builder performs is keyed by top-level section title. #pragma once #include @@ -31,6 +27,25 @@ namespace core { using json = nlohmann::json; +/// Standalone fluent builder for a structured POM prompt. +/// +/// A thin, section-oriented wrapper over ``signalwire::pom::PromptObjectModel`` +/// for callers who want to assemble a prompt tree outside an agent — there are +/// no predefined section types, sections are created on demand by title, and +/// every mutator returns ``*this`` for chaining. The finished prompt renders to +/// Markdown (``render_markdown``), XML (``render_xml``), or the section-array +/// JSON the SWML ``ai.prompt`` field takes (``to_dict`` / ``to_json``); +/// ``from_sections`` reconstructs a builder from that array. +/// +/// ``add_to_section`` and ``add_subsection`` AUTO-VIVIFY: naming a section that +/// does not exist creates it rather than failing, so prompt assembly need not +/// be ordered. Bodies appended to an existing section are separated from the +/// prior body by a blank line. +/// +/// Sections are resolved by title on each access rather than cached, so a +/// ``pom::Section*`` handed back by ``get_section`` follows the usual +/// container-invalidation contract — it is invalidated by any later mutation +/// that grows the section list (the same rule as ``std::vector::data()``). class PomBuilder { public: /// Initialize a new POM builder with an empty POM. diff --git a/include/signalwire/core/post_prompt_generated/post_prompt_swaig_log_entry.hpp b/include/signalwire/core/post_prompt_generated/post_prompt_swaig_log_entry.hpp index 9867884..63bcca5 100644 --- a/include/signalwire/core/post_prompt_generated/post_prompt_swaig_log_entry.hpp +++ b/include/signalwire/core/post_prompt_generated/post_prompt_swaig_log_entry.hpp @@ -34,8 +34,8 @@ struct PostPromptSwaigLogEntry { std::optional delayed_post_response; std::optional mcp_url; std::optional mcp_tool; - std::optional mcp_response; - std::optional mcp_error; + std::optional mcp_response; + std::optional mcp_error; json extras = json::object(); }; diff --git a/include/signalwire/core/post_prompt_generated/post_prompt_system_log_entry.hpp b/include/signalwire/core/post_prompt_generated/post_prompt_system_log_entry.hpp index ae9968b..5c2d037 100644 --- a/include/signalwire/core/post_prompt_generated/post_prompt_system_log_entry.hpp +++ b/include/signalwire/core/post_prompt_generated/post_prompt_system_log_entry.hpp @@ -31,9 +31,6 @@ struct PostPromptSystemLogEntry { std::optional tokens; std::optional content_type; std::optional metadata; - std::optional context; - std::optional step; - std::optional step_index; json extras = json::object(); }; diff --git a/include/signalwire/core/security_config.hpp b/include/signalwire/core/security_config.hpp index cce60b4..9aa425f 100644 --- a/include/signalwire/core/security_config.hpp +++ b/include/signalwire/core/security_config.hpp @@ -3,12 +3,9 @@ // // Unified security configuration for SignalWire services. // -// C++ port of the Python reference -// ``signalwire.core.security_config.SecurityConfig`` (cross-checked against the -// Java ``com.signalwire.sdk.core.SecurityConfig``). Provides centralized -// security settings (SSL, allowed hosts, CORS, security headers, basic auth) -// consumed by the web/agent services so behavior stays consistent. Defaults are -// applied first, then environment variables (backward compatibility), then a +// Provides centralized security settings (SSL, allowed hosts, CORS, security +// headers, basic auth) consumed by the web/agent services so behavior stays +// consistent. Defaults are applied first, then environment variables, then a // config file if available (highest priority). #pragma once @@ -23,15 +20,46 @@ namespace core { using json = nlohmann::json; /// Result of ``SecurityConfig::validate_ssl_config``: a validity flag plus an -/// optional error message (Python returns ``(bool, str | None)``). +/// error message that is set only when ``valid`` is false. struct SslValidationResult { bool valid = false; std::optional error; }; +/// Centralized security settings for a SignalWire service — SSL, allowed +/// hosts, CORS, response security headers, request limits, and basic-auth +/// credentials. +/// +/// The web/agent services read their security posture from one of these so the +/// behaviour is consistent across them. Settings are resolved in three layers, +/// each overriding the last: **built-in defaults**, then the ``SWML_*`` +/// **environment variables** named by the class constants below, then a +/// **config file**'s ``security`` section (highest priority) — located either +/// from an explicit path or from the service name. +/// +/// Security-relevant behaviours worth knowing before you deploy: +/// * ``get_basic_auth`` never returns an empty password. When none is +/// configured it GENERATES a random one and warns once — that password +/// lives only in this process, so external callers who do not know it get +/// HTTP 401. Configure ``SWML_BASIC_AUTH_USER``/``_PASSWORD`` for anything +/// a client must reach. +/// * ``validate_ssl_config`` is always valid when SSL is disabled; with SSL +/// enabled it requires cert and key paths that are set AND exist on disk, +/// and ``get_ssl_context_kwargs`` returns an EMPTY object when SSL is off +/// OR that validation fails (logging the reason) — so a caller that binds +/// TLS only on a non-empty result will not silently serve plaintext. +/// * ``should_allow_host`` treats ``*`` in the allowed list as allow-all. +/// * ``get_security_headers`` adds ``Strict-Transport-Security`` only when +/// the caller says the connection is HTTPS and HSTS is enabled — sending +/// HSTS over plaintext is meaningless and can lock out a host. +/// * ``log_config`` never logs a secret. +/// +/// Defaults: SSL off, verify mode ``CERT_REQUIRED``, 10 MiB max request, 60 +/// requests/min rate limit, 30 s request timeout, HSTS on with a one-year +/// max-age. class SecurityConfig { public: - // Security environment variable names (mirror the Python class constants). + // Security environment variable names. static constexpr const char* SSL_ENABLED = "SWML_SSL_ENABLED"; static constexpr const char* SSL_CERT_PATH = "SWML_SSL_CERT_PATH"; static constexpr const char* SSL_KEY_PATH = "SWML_SSL_KEY_PATH"; @@ -53,8 +81,8 @@ class SecurityConfig { explicit SecurityConfig(const std::optional& config_file = std::nullopt, const std::optional& service_name = std::nullopt); - /// Load configuration from environment variables (public; part of the - /// Python surface — called by the ctor and re-callable). + /// Load configuration from environment variables. Called by the constructor, + /// and safe to call again to re-read the environment. void load_from_env(); /// Validate SSL configuration. When SSL is disabled the result is always @@ -62,10 +90,10 @@ class SecurityConfig { [[nodiscard]] SslValidationResult validate_ssl_config() const; /// SSL options for binding an HTTPS server. Empty when SSL is disabled or - /// validation fails; otherwise a language-neutral option object with keys - /// ``ssl_enabled``, ``cert_path``, ``key_path`` (Python returns uvicorn - /// ``ssl_certfile``/``ssl_keyfile`` kwargs; the C++/Java idiom is a neutral - /// map the web service consumes). + /// validation fails; otherwise EXACTLY two keys and nothing else — + /// ``ssl_certfile`` (the cert path) and ``ssl_keyfile`` (the key path). It is + /// deliberately NOT a ``{ssl_enabled, cert_path, key_path}`` map; the key + /// spelling is part of the contract and is pinned by a test. [[nodiscard]] json get_ssl_context_kwargs() const; /// Get basic auth credentials, generating a random URL-safe password when @@ -88,7 +116,7 @@ class SecurityConfig { /// Log the current security configuration (never logs secrets). void log_config(const std::string& service_name) const; - // Accessors (matches the Python public attributes). + // Accessors. [[nodiscard]] bool ssl_enabled() const { return ssl_enabled_; } [[nodiscard]] const std::optional& ssl_cert_path() const { return ssl_cert_path_; } [[nodiscard]] const std::optional& ssl_key_path() const { return ssl_key_path_; } diff --git a/include/signalwire/core/swaig_actions_generated/context_switch_action.hpp b/include/signalwire/core/swaig_actions_generated/context_switch_action.hpp index ac0654b..0bf0acf 100644 --- a/include/signalwire/core/swaig_actions_generated/context_switch_action.hpp +++ b/include/signalwire/core/swaig_actions_generated/context_switch_action.hpp @@ -23,12 +23,12 @@ using json = nlohmann::json; /// /// Method-less DTO: one typed member per snake wire key + open `extras`. struct ContextSwitchAction { - std::optional system_prompt; - std::optional user_prompt; - std::optional system_pom; - std::optional user_pom; std::optional consolidate; std::optional full_reset; + std::optional system_pom; + std::optional system_prompt; + std::optional user_pom; + std::optional user_prompt; json extras = json::object(); }; diff --git a/include/signalwire/core/swaig_actions_generated/hold_action.hpp b/include/signalwire/core/swaig_actions_generated/hold_action.hpp index b93d14c..d491028 100644 --- a/include/signalwire/core/swaig_actions_generated/hold_action.hpp +++ b/include/signalwire/core/swaig_actions_generated/hold_action.hpp @@ -23,7 +23,7 @@ using json = nlohmann::json; /// /// Method-less DTO: one typed member per snake wire key + open `extras`. struct HoldAction { - std::optional timeout; + std::optional timeout; json extras = json::object(); }; diff --git a/include/signalwire/core/swaig_actions_generated/playback_bg_action.hpp b/include/signalwire/core/swaig_actions_generated/playback_bg_action.hpp index 1cdea80..1287620 100644 --- a/include/signalwire/core/swaig_actions_generated/playback_bg_action.hpp +++ b/include/signalwire/core/swaig_actions_generated/playback_bg_action.hpp @@ -23,7 +23,7 @@ using json = nlohmann::json; /// /// Method-less DTO: one typed member per snake wire key + open `extras`. struct PlaybackBgAction { - std::optional file; + std::optional file; std::optional wait; json extras = json::object(); }; diff --git a/include/signalwire/core/swaig_actions_generated/swaig_action.hpp b/include/signalwire/core/swaig_actions_generated/swaig_action.hpp new file mode 100644 index 0000000..4641030 --- /dev/null +++ b/include/signalwire/core/swaig_actions_generated/swaig_action.hpp @@ -0,0 +1,63 @@ +// Copyright (c) 2025 SignalWire +// SPDX-License-Identifier: MIT +// +// Code generated by scripts/generate_swaig_payloads.py; DO NOT EDIT. +// +// swaig-response components/schemas 'SwaigAction'. A response-action object. The keys below are the +// full vocabulary dispatched by actions.c::process_action; an action object sets one or more of +// them. Each key's source line is the engine dispatch site. +#pragma once + +#include +#include +#include +#include +#include + +namespace signalwire { +namespace core { +namespace swaig_actions_generated { + +using json = nlohmann::json; + +/// SwaigAction — generated read-side data type. +/// swaig-response components/schemas 'SwaigAction'. A response-action object. The keys below are +/// the full vocabulary dispatched by actions.c::process_action; an action object sets one or more +/// of them. Each key's source line is the engine dispatch site. +/// +/// Method-less DTO: one typed member per snake wire key + open `extras`. +struct SwaigAction { + std::optional SWML; + std::optional add_dynamic_hints; + std::optional back_to_back_functions; + std::optional change_context; + std::optional change_step; + std::optional clear_dynamic_hints; + std::optional context_switch; + std::optional end_of_speech_timeout; + std::optional extensive_data; + std::optional functions_on_speaker_timeout; + std::optional hangup; + std::optional hold; + std::optional playback_bg; + std::optional replace_in_history; + std::optional say; + std::optional set_global_data; + std::optional set_meta_data; + std::optional settings; + std::optional speech_event_timeout; + std::optional stop; + std::optional stop_playback_bg; + std::optional toggle_functions; + std::optional transfer; + std::optional unset_global_data; + std::optional unset_meta_data; + std::optional user_event; + std::optional user_input; + std::optional wait_for_user; + json extras = json::object(); +}; + +} // namespace swaig_actions_generated +} // namespace core +} // namespace signalwire diff --git a/include/signalwire/core/swaig_actions_generated/swaig_response.hpp b/include/signalwire/core/swaig_actions_generated/swaig_response.hpp new file mode 100644 index 0000000..070b52a --- /dev/null +++ b/include/signalwire/core/swaig_actions_generated/swaig_response.hpp @@ -0,0 +1,34 @@ +// Copyright (c) 2025 SignalWire +// SPDX-License-Identifier: MIT +// +// Code generated by scripts/generate_swaig_payloads.py; DO NOT EDIT. +// +// swaig-response components/schemas 'SwaigResponse'. Parsed at actions.c:2228-2276. +#pragma once + +#include +#include +#include +#include +#include + +namespace signalwire { +namespace core { +namespace swaig_actions_generated { + +using json = nlohmann::json; + +/// SwaigResponse — generated read-side data type. +/// swaig-response components/schemas 'SwaigResponse'. Parsed at actions.c:2228-2276. +/// +/// Method-less DTO: one typed member per snake wire key + open `extras`. +struct SwaigResponse { + std::optional response; + std::optional action; + std::optional post_process; + json extras = json::object(); +}; + +} // namespace swaig_actions_generated +} // namespace core +} // namespace signalwire diff --git a/include/signalwire/core/swaig_actions_generated/transfer_action.hpp b/include/signalwire/core/swaig_actions_generated/transfer_action.hpp index 9d0432f..dfdeb58 100644 --- a/include/signalwire/core/swaig_actions_generated/transfer_action.hpp +++ b/include/signalwire/core/swaig_actions_generated/transfer_action.hpp @@ -23,7 +23,7 @@ using json = nlohmann::json; /// /// Method-less DTO: one typed member per snake wire key + open `extras`. struct TransferAction { - std::optional dest; + std::optional dest; std::optional summarize; json extras = json::object(); }; diff --git a/include/signalwire/core/swaig_function.hpp b/include/signalwire/core/swaig_function.hpp index 4c2f025..9fbef39 100644 --- a/include/signalwire/core/swaig_function.hpp +++ b/include/signalwire/core/swaig_function.hpp @@ -3,15 +3,12 @@ // // SWAIGFunction — a registered SWAIG function (a tool the AI model can call). // -// Mirrors the Python reference signalwire.core.swaig_function.SWAIGFunction and -// the Java port com.signalwire.sdk.swaig.SWAIGFunction. It holds a -// name/description/parameters/handler and renders into the SWAIG JSON -// descriptor sent to the model. +// It holds a name/description/parameters/handler and renders into the SWAIG +// JSON descriptor sent to the model. // -// This is the core.swaig_function reference class. It is DISTINCT from the -// lightweight swaig::ToolDefinition struct used by Service/AgentBase — that one -// is a plain wire-descriptor holder; SWAIGFunction adds execute(), validate, -// __call__, and the full keyword-arg constructor surface of the reference. +// DISTINCT from the lightweight swaig::ToolDefinition struct used by +// Service/AgentBase — that one is a plain wire-descriptor holder; SWAIGFunction +// adds execute(), validate_args(), and direct invocation via call()/operator(). #pragma once @@ -27,8 +24,7 @@ namespace core { using json = nlohmann::json; -/// Result of validate_args: (is_valid, errors). Mirrors the reference's Python -/// `tuple[bool, list[str]]` / Java ValidationResult. +/// Result of validate_args: (is_valid, errors). struct ArgsValidationResult { bool valid = false; std::vector errors; @@ -36,7 +32,7 @@ struct ArgsValidationResult { /// Handler signature: (args, raw_data) -> result. The result JSON may already /// be a FunctionResult dict (containing "response"), any other object, or a -/// scalar coerced via to-string — matching the reference's execute() coercion. +/// scalar, which execute() coerces via to-string. using SwaigFunctionHandler = std::function; /// Represents a SWAIG function — a tool the AI model can call. @@ -44,10 +40,9 @@ class SWAIGFunction { public: /// Construct a SWAIG function. /// - /// The reference constructor takes many keyword args with defaults plus - /// `**extra_swaig_fields`. In C++ the required trio (name, handler, - /// description) are leading params; the remaining optionals default, and - /// `extra_swaig_fields` is a trailing JSON object (the kwargs idiom). + /// The required trio (name, handler, description) are leading params; every + /// remaining param is defaulted. Arbitrary additional SWAIG descriptor fields + /// go in the trailing `extra_swaig_fields` JSON object. SWAIGFunction(std::string name, SwaigFunctionHandler handler, std::string description, json parameters = json::object(), bool secure = false, std::optional fillers = std::nullopt, @@ -57,7 +52,7 @@ class SWAIGFunction { std::vector required = {}, bool is_typed_handler = false, json extra_swaig_fields = json::object()); - // ---- Accessors (matches Python instance attributes) ---- + // ---- Accessors ---- [[nodiscard]] const std::string& name() const { return name_; } [[nodiscard]] const std::string& description() const { return description_; } [[nodiscard]] const json& parameters() const { return parameters_; } @@ -72,26 +67,23 @@ class SWAIGFunction { [[nodiscard]] bool is_external() const { return is_external_; } [[nodiscard]] const SwaigFunctionHandler& handler() const { return handler_; } - /// Call the underlying handler. C++ analog of the reference's `__call__` - /// (which makes the object callable). Returns the handler's raw (uncoerced) + /// Call the underlying handler. Returns the handler's raw (uncoerced) /// return value. Exposed both as a named `call` and as `operator()`. json call(const json& args, const json& raw_data = json::object()) const; json operator()(const json& args, const json& raw_data = json::object()) const; /// Execute the function: invoke the handler and coerce its return value into /// a FunctionResult dict. On any exception, logs and returns a generic - /// non-leaking error message (matches the reference's try/except). + /// non-leaking error message rather than propagating the throw. [[nodiscard]] json execute(const json& args, const std::optional& raw_data = std::nullopt) const; /// Validate the arguments against the parameter schema. /// - /// The Python reference tries jsonschema_rs / jsonschema and, when neither is - /// installed, SKIPS validation (returns (true, [])). C++ has no bundled - /// JSON-Schema validator, so this performs the always-available built-in - /// check: the schema's `required` list plus each declared property's `type` - /// (matches the Java port's built-in fallback). Passes when no properties - /// are declared. + /// There is no bundled JSON-Schema validator, so this performs a built-in + /// check rather than full schema validation: the schema's `required` list + /// plus each declared property's `type`. Passes when no properties are + /// declared. [[nodiscard]] ArgsValidationResult validate_args(const json& args) const; /// Convert this function to a SWAIG-compatible JSON descriptor for SWML. diff --git a/include/signalwire/core/swaig_request_generated/swaig_request.hpp b/include/signalwire/core/swaig_request_generated/swaig_request.hpp index 523c949..6eeb2ed 100644 --- a/include/signalwire/core/swaig_request_generated/swaig_request.hpp +++ b/include/signalwire/core/swaig_request_generated/swaig_request.hpp @@ -23,6 +23,8 @@ using json = nlohmann::json; /// /// Method-less DTO: one typed member per snake wire key + open `extras`. struct SwaigRequest { + std::optional SWMLCall; + std::optional SWMLVars; std::optional ai_session_id; std::optional app_name; std::optional args; diff --git a/include/signalwire/core/swml_builder.hpp b/include/signalwire/core/swml_builder.hpp index 5ed4c60..4d59afa 100644 --- a/include/signalwire/core/swml_builder.hpp +++ b/include/signalwire/core/swml_builder.hpp @@ -3,17 +3,13 @@ // // SWMLBuilder — fluent builder for SWML documents. // -// Mirrors the Python reference signalwire.core.swml_builder.SWMLBuilder (which -// wraps an SWMLService) and the Java port com.signalwire.sdk.swml.SWMLBuilder. -// It delegates to an underlying swml::Service instance (the C++ analog of the -// reference's SWMLService) for the actual document construction; each verb -// method appends a verb to the main section and returns *this for chaining. +// It delegates to an underlying swml::Service instance for the actual document +// construction; each verb method appends a verb to the main section and returns +// *this for chaining. // -// The reference installs the remaining schema verbs dynamically via __getattr__ -// at runtime. C++ has no __getattr__ / method_missing analog, so that dynamic -// dispatch is intentionally NOT ported — the explicit verb helpers below cover -// the reference's named verb methods (answer/hangup/ai/play/say), matching the -// enumerated method surface. +// The builder exposes explicit helpers for answer / hangup / ai / play / say. +// Any other schema verb is added through the underlying ``swml::Service``, +// reachable via ``service()``. #pragma once diff --git a/include/signalwire/core/swml_handler.hpp b/include/signalwire/core/swml_handler.hpp index cd82296..3493fdd 100644 --- a/include/signalwire/core/swml_handler.hpp +++ b/include/signalwire/core/swml_handler.hpp @@ -3,10 +3,8 @@ // // SWML verb handlers — the pluggable verb-handler registry. // -// Mirrors the Python reference signalwire.core.swml_handler (SWMLVerbHandler -// abstract base, AIVerbHandler concrete handler for the complex "ai" verb, and -// VerbHandlerRegistry mapping verb-name -> handler) and the Java port -// (com.signalwire.sdk.swml.{SWMLVerbHandler,AIVerbHandler,VerbHandlerRegistry}). +// SWMLVerbHandler is the abstract base, AIVerbHandler the concrete handler for +// the complex "ai" verb, and VerbHandlerRegistry maps verb-name -> handler. // // A verb handler provides specialized logic for complex SWML verbs that cannot // be handled generically: it names its verb, validates a config, and builds a @@ -29,9 +27,8 @@ using json = nlohmann::json; /// Result of validate_config: (is_valid, error_messages). /// -/// C++ analog of the reference's Python `tuple[bool, list[str]]` / Java -/// ValidationResult. `valid` is redundant with `errors.empty()` but kept as an -/// explicit field so the (bool, list) tuple shape is preserved 1:1. +/// `valid` is redundant with `errors.empty()` but kept as an explicit field so +/// the (bool, list) pair shape is available directly. struct VerbValidationResult { bool valid = false; std::vector errors; @@ -39,9 +36,8 @@ struct VerbValidationResult { /// Base interface for SWML verb handlers. /// -/// Abstract (pure-virtual) — the C++ analog of Python's @abstractmethod and -/// Java's UnsupportedOperationException stubs: a subclass that forgets to -/// override fails to compile/link. +/// Every method is pure-virtual, so a subclass that forgets to override one +/// fails to compile. class SWMLVerbHandler { public: virtual ~SWMLVerbHandler() = default; @@ -52,10 +48,8 @@ class SWMLVerbHandler { /// Validate the configuration for this verb. [[nodiscard]] virtual VerbValidationResult validate_config(const json& config) const = 0; - /// Build a configuration for this verb from the provided keyword arguments. - /// - /// The reference takes `**kwargs`; in C++ that lands as a JSON object of - /// named arguments (the kwargs idiom). Returns the verb config object. + /// Build a configuration for this verb from the provided named arguments, + /// passed as a JSON object. Returns the verb config object. [[nodiscard]] virtual json build_config(const json& kwargs = json::object()) const = 0; }; @@ -74,11 +68,11 @@ class AIVerbHandler : public SWMLVerbHandler { /// Catch-all kwargs form — extracts the recognized keys (prompt_text, /// prompt_pom, contexts, post_prompt, post_prompt_url, swaig) from the JSON - /// object and treats the rest as extra AI params. Mirrors the Java map-based - /// buildConfig(kwargs). Prefer the typed overload below. + /// object and treats the rest as extra AI params. Prefer the typed overload + /// below. [[nodiscard]] json build_config(const json& kwargs = json::object()) const override; - /// Typed overload mirroring the Python signature 1:1. Requires exactly one of + /// Typed overload. Requires exactly one of /// prompt_text / prompt_pom (mutually exclusive, else throws /// std::invalid_argument). `languages`, `hints`, `pronounce`, `global_data` /// go at the top level; every other extra kwarg lands in config["params"]. @@ -109,8 +103,7 @@ class VerbHandlerRegistry { /// Whether a handler exists for a verb. [[nodiscard]] bool has_handler(const std::string& verb_name) const; - /// The registered verb names, sorted (Python: - /// ``sorted(VerbHandlerRegistry._handlers.keys())``). + /// The registered verb names, sorted. [[nodiscard]] std::vector get_verb_names() const { std::vector names; names.reserve(handlers_.size()); diff --git a/include/signalwire/core/swml_renderer.hpp b/include/signalwire/core/swml_renderer.hpp index 394834d..3cad231 100644 --- a/include/signalwire/core/swml_renderer.hpp +++ b/include/signalwire/core/swml_renderer.hpp @@ -3,15 +3,13 @@ // // SwmlRenderer — SWML document rendering utilities. // -// Mirrors the Python reference signalwire.core.swml_renderer.SwmlRenderer (two -// static helpers) and the Java port com.signalwire.sdk.swml.SwmlRenderer. Both -// helpers are static; they build a document on a swml::Service (via SWMLBuilder) -// and return the rendered SWML string. +// Two static helpers; both build a document on a swml::Service (via +// SWMLBuilder) and return the rendered SWML string. // -// render_swml has many optional inputs. The reference passes them as keyword -// args; the C++ idiom for that is a RenderOptions struct (named fields with -// reference defaults), mirroring the Java RenderOptions builder object. A -// convenience minimal-form overload covers the common (prompt, service) call. +// render_swml has many optional inputs, so they are gathered into a +// RenderOptions struct of named fields with defaults rather than a long +// positional parameter list. A convenience minimal-form overload covers the +// common (prompt, service) call. #pragma once @@ -27,9 +25,9 @@ namespace core { using json = nlohmann::json; -/// Named-parameter options for SwmlRenderer::render_swml — the C++ analog of the -/// reference's keyword arguments. The two required inputs (prompt, service) are -/// passed to render_swml directly; the rest live here with reference defaults. +/// Named-parameter options for SwmlRenderer::render_swml. The two required +/// inputs (prompt, service) are passed to render_swml directly; every other +/// input lives here with a default. struct RenderOptions { std::optional post_prompt; std::optional post_prompt_url; @@ -58,7 +56,9 @@ class SwmlRenderer { const RenderOptions& opts = {}); /// Generate a SWML document for a function response — a `play` of the - /// response text followed by any provided actions. + /// response text followed by any provided actions. The response text is + /// emitted as `play: {url: "say:"}`: the SWML `play` verb has no + /// `text` key, so the `say:` URL scheme is how spoken text reaches the wire. [[nodiscard]] static std::string render_function_response_swml( const std::string& response_text, swml::Service& service, const std::optional>& actions = std::nullopt, diff --git a/include/signalwire/datamap/datamap.hpp b/include/signalwire/datamap/datamap.hpp index 9e22f5c..68c62ef 100644 --- a/include/signalwire/datamap/datamap.hpp +++ b/include/signalwire/datamap/datamap.hpp @@ -20,10 +20,9 @@ class DataMap { public: explicit DataMap(const std::string& function_name); - /// The SWAIG function name this data-map defines (reference: - /// ``self.function_name``) — emitted as the ``function`` key and used as the - /// fallback description. The caller names it at construction, so the caller - /// can read it back. + /// The SWAIG function name this data-map defines — emitted as the + /// ``function`` key and used as the fallback description. The caller names it + /// at construction, so the caller can read it back. [[nodiscard]] const std::string& function_name() const { return function_name_; } /// Set the LLM-facing tool description (the "purpose"). PROMPT @@ -83,10 +82,7 @@ class DataMap { /// Add expressions that run after the most recent webhook DataMap& webhook_expressions(const std::vector& expressions); - /// Set request body for the last added webhook - DataMap& body(const json& data); - - /// Set request params for the last added webhook (alias for body) + /// Set request params for the last added webhook (POST/PUT request data too) DataMap& params(const json& data); /// Set foreach configuration for the last webhook diff --git a/include/signalwire/logging.hpp b/include/signalwire/logging.hpp index d6b2c00..1a18e06 100644 --- a/include/signalwire/logging.hpp +++ b/include/signalwire/logging.hpp @@ -7,10 +7,37 @@ #include #include +// For the control-char scrub applied on the emission path below. logging_config +// includes signalwire/logging/logger.hpp (a DIFFERENT header from this one), so +// this does not close an include cycle. +#include "signalwire/core/logging_config.hpp" + namespace signalwire { enum class LogLevel { Debug = 0, Info = 1, Warn = 2, Error = 3, Off = 4 }; +/// The SDK's process-wide logging singleton. +/// +/// One instance per process, reached through ``Logger::instance()`` (or the +/// ``get_logger()`` free function); non-copyable. Every operation is guarded by +/// an internal mutex, so concurrent logging from the agent's HTTP threads and +/// the RELAY reader thread is safe. Records at or above ``level()`` are +/// emitted, with ``Warn`` and ``Error`` going to ``stderr`` and the rest to +/// ``stdout``; ``suppress()`` silences output entirely without disturbing the +/// configured level (``unsuppress()`` restores it). +/// +/// Initial state comes from the environment at first use: +/// ``SIGNALWIRE_LOG_LEVEL`` (``debug``/``info``/``warn``/``error``, default +/// ``Info``) and ``SIGNALWIRE_LOG_MODE=off``, which starts the logger +/// suppressed. +/// +/// Every message is scrubbed of control characters ON THE EMISSION PATH before +/// it is written. This is log-injection defence: without it a caller-supplied +/// ``\x00`` or ``\x1b[`` escape reaches the terminal verbatim and can forge log +/// lines. +/// +/// Distinct from ``signalwire::logging::Logger`` (``signalwire/logging/ +/// logger.hpp``), which is a per-component NAMED logger created by value. class Logger { public: static Logger& instance() { @@ -64,10 +91,17 @@ class Logger { break; } + // Scrub control characters BEFORE emitting — log-injection defence. Merely + // EXPOSING the scrub without putting it on the emission path offers no + // protection at all: a caller-supplied `\x00` or a `\x1b[` escape reaches + // the terminal verbatim and can forge log lines. + const std::string safe = + ::signalwire::core::logging_config::strip_control_chars_str(std::string(message)); + if (level >= LogLevel::Warn) { - std::cerr << prefix << message << "\n"; + std::cerr << prefix << safe << "\n"; } else { - std::cout << prefix << message << "\n"; + std::cout << prefix << safe << "\n"; } } diff --git a/include/signalwire/logging/logger.hpp b/include/signalwire/logging/logger.hpp index 601096a..5fdbc6e 100644 --- a/include/signalwire/logging/logger.hpp +++ b/include/signalwire/logging/logger.hpp @@ -13,17 +13,45 @@ namespace logging { enum class LogLevel { DEBUG, INFO, WARN, ERROR, OFF }; [[nodiscard]] inline LogLevel get_log_level() { - std::string level = ""; + std::string level; const char* env = std::getenv("SIGNALWIRE_LOG_LEVEL"); - if (env) level = env; + if (env) { + level = env; + } const char* mode = std::getenv("SIGNALWIRE_LOG_MODE"); - if (mode && std::string(mode) == "off") return LogLevel::OFF; - if (level == "debug") return LogLevel::DEBUG; - if (level == "warn") return LogLevel::WARN; - if (level == "error") return LogLevel::ERROR; + if (mode && std::string(mode) == "off") { + return LogLevel::OFF; + } + if (level == "debug") { + return LogLevel::DEBUG; + } + if (level == "warn") { + return LogLevel::WARN; + } + if (level == "error") { + return LogLevel::ERROR; + } return LogLevel::INFO; } +/// A named, per-component logger created by value. +/// +/// Construct one per subsystem (or via ``get_logger("name")``) and the name is +/// stamped into every line: ``[LEVEL][name] message``. Cheap to copy and hold +/// as a member — it carries only its name; there is no shared state, no mutex, +/// and no registry. +/// +/// The threshold is NOT stored on the instance: each call re-reads +/// ``get_log_level()``, which derives the level from ``SIGNALWIRE_LOG_LEVEL`` +/// (``debug``/``warn``/``error``, defaulting to ``INFO``) and from +/// ``SIGNALWIRE_LOG_MODE=off``, which turns everything off. So a change to the +/// environment takes effect on the next call rather than at construction. All +/// levels write to ``stderr``, and the logging methods are ``const``. +/// +/// Distinct from ``signalwire::Logger`` (``signalwire/logging.hpp``), which is +/// the mutex-guarded, suppressible process singleton that scrubs control +/// characters on emission. This one does neither — it is the lightweight +/// component-tagged logger. class Logger { public: explicit Logger(const std::string& name) : name_(name) {} @@ -33,20 +61,24 @@ class Logger { // or a substring view with no allocation. (The constructor's `name` // stays std::string: it is retained in name_.) void debug(std::string_view msg) const { - if (get_log_level() <= LogLevel::DEBUG) - std::cerr << "[DEBUG][" << name_ << "] " << msg << std::endl; + if (get_log_level() <= LogLevel::DEBUG) { + std::cerr << "[DEBUG][" << name_ << "] " << msg << "\n"; + } } void info(std::string_view msg) const { - if (get_log_level() <= LogLevel::INFO) - std::cerr << "[INFO][" << name_ << "] " << msg << std::endl; + if (get_log_level() <= LogLevel::INFO) { + std::cerr << "[INFO][" << name_ << "] " << msg << "\n"; + } } void warn(std::string_view msg) const { - if (get_log_level() <= LogLevel::WARN) - std::cerr << "[WARN][" << name_ << "] " << msg << std::endl; + if (get_log_level() <= LogLevel::WARN) { + std::cerr << "[WARN][" << name_ << "] " << msg << "\n"; + } } void error(std::string_view msg) const { - if (get_log_level() <= LogLevel::ERROR) - std::cerr << "[ERROR][" << name_ << "] " << msg << std::endl; + if (get_log_level() <= LogLevel::ERROR) { + std::cerr << "[ERROR][" << name_ << "] " << msg << "\n"; + } } private: diff --git a/include/signalwire/pom/pom.hpp b/include/signalwire/pom/pom.hpp index 9e7fb8c..8dcae22 100644 --- a/include/signalwire/pom/pom.hpp +++ b/include/signalwire/pom/pom.hpp @@ -1,7 +1,7 @@ // Copyright (c) 2025 SignalWire // SPDX-License-Identifier: MIT // -// Prompt Object Model (POM) — C++ port of signalwire/pom/pom.py. +// Prompt Object Model (POM). // // A structured data format for composing, organizing, and rendering prompt // instructions for large language models. The POM provides a tree-based @@ -18,13 +18,13 @@ // * YAML via ``to_yaml`` / ``from_yaml`` (minimal in-tree YAML I/O — POM // content is always a list of dicts whose values are strings, bools, or // lists; no anchors, tags, or free-form scalars to handle). -// * Markdown via ``render_markdown`` — exact byte-for-byte with Python's -// ``Section.render_markdown`` / ``PromptObjectModel.render_markdown``. -// * XML via ``render_xml`` — exact byte-for-byte with Python's renderers. +// * Markdown via ``Section::render_markdown`` / +// ``PromptObjectModel::render_markdown``. +// * XML via ``render_xml``. // -// Rendering contract: output strings match the reference verbatim (including -// trailing newlines, joiners, and section/bullet numbering rules). The C++ -// tests in ``tests/test_pom.cpp`` are written from those Python outputs. +// Rendering contract: the rendered output is byte-exact — trailing newlines, +// joiners, and section/bullet numbering rules are all part of the contract and +// are pinned by the tests in ``tests/test_pom.cpp``. #pragma once #include @@ -42,14 +42,13 @@ class PromptObjectModel; // fwd /// One section in the Prompt Object Model tree. /// -/// Mirrors Python's ``signalwire.pom.pom.Section``. Fields are public to -/// match the Python attribute access pattern ``section.body``, +/// Fields are public and mutated directly: ``section.body``, /// ``section.bullets``, ``section.subsections``. class Section { public: - /// Section title. Optional only on the very first top-level section - /// (Python enforces "only the first section can have no title"); for - /// subsections a title is always required. + /// Section title. Optional only on the very first top-level section — + /// only the first section may have no title; for subsections a title is + /// always required. std::optional title; /// Optional paragraph of body text. @@ -62,7 +61,7 @@ class Section { std::vector
subsections; /// Whether this section participates in section numbering. Three-state: - /// * ``std::nullopt`` — not specified (Python ``None``); inherits. + /// * ``std::nullopt`` — not specified; inherits. /// * ``true`` — explicitly numbered. /// * ``false`` — explicitly opted out of numbering. /// Numbering is "all-or-none per sibling group": if any sibling has @@ -83,8 +82,7 @@ class Section { std::vector bs = {}, std::optional num = std::nullopt, bool numbered_bullets = false); - /// Replace (NOT append) the body text. Mirrors Python's documented - /// "Add OR REPLACE the body text" contract. + /// Replace (NOT append) the body text. void add_body(const std::string& b); /// Append bullets to the existing list. @@ -92,20 +90,24 @@ class Section { /// Add a child subsection. Returns a reference to the newly-created /// subsection so callers can chain further mutations. - /// Throws ``std::invalid_argument`` if ``title`` is empty (Python raises - /// ``ValueError("Subsections must have a title")``). + /// Throws ``std::invalid_argument`` if ``title`` is empty ("Subsections must + /// have a title"). + /// ``numbered`` is BINARY here, not tri-state: it defaults to ``false`` and + /// is passed straight through, so a subsection built this way is never + /// unset. (The ``Section`` constructor and ``PromptObjectModel::add_section`` + /// DO take the tri-state ``std::optional``.) The distinction is + /// load-bearing in ``render_markdown``: an UNSET sibling inherits numbering + /// from the group, while an explicit ``false`` opts out. Section& add_subsection(const std::string& title, const std::string& body = "", - const std::vector& bullets = {}, - std::optional numbered = std::nullopt, + const std::vector& bullets = {}, bool numbered = false, bool numbered_bullets = false); - /// Convert the section (and its subtree) to a JSON object. Matches the - /// Python key order: title, body, bullets, subsections, numbered, + /// Convert the section (and its subtree) to a JSON object. Keys are emitted + /// in this order: title, body, bullets, subsections, numbered, /// numberedBullets. [[nodiscard]] json to_json() const; - /// Python-compatible alias for to_json — Python exposes ``to_dict``. - /// Returns the same JSON object. + /// Alias for ``to_json``. Returns the same JSON object. [[nodiscard]] json to_dict() const { return to_json(); } /// Render this section + subtree as Markdown. ``level`` is the heading @@ -142,9 +144,8 @@ class PromptObjectModel { [[nodiscard]] static PromptObjectModel from_yaml(const std::string& yaml_text); /// Append a new top-level section. ``title`` may be empty *only* for - /// the very first section (Python enforces "Only the first section can - /// have no title"); subsequent calls without a title throw - /// ``std::invalid_argument``. + /// the very first section — only the first section may have no title; + /// subsequent calls without a title throw ``std::invalid_argument``. Section& add_section(const std::string& title = "", const std::string& body = "", const std::vector& bullets = {}, std::optional numbered = std::nullopt, bool numbered_bullets = false); @@ -157,8 +158,8 @@ class PromptObjectModel { [[nodiscard]] Section* find_section(const std::string& title); [[nodiscard]] const Section* find_section(const std::string& title) const; - /// Whole-tree JSON serializer. Returns a pretty-printed (indent=2) - /// JSON array string, matching Python's ``json.dumps(..., indent=2)``. + /// Whole-tree JSON serializer. Returns a pretty-printed JSON array string + /// (2-space indent). [[nodiscard]] std::string to_json() const; /// Whole-tree YAML serializer. Returns a YAML document representing diff --git a/include/signalwire/prefabs/prefabs.hpp b/include/signalwire/prefabs/prefabs.hpp index 4e34ecf..940fb33 100644 --- a/include/signalwire/prefabs/prefabs.hpp +++ b/include/signalwire/prefabs/prefabs.hpp @@ -17,15 +17,15 @@ using json = nlohmann::json; class InfoGathererAgent : public agent::AgentBase { public: explicit InfoGathererAgent(const std::string& name = "info_gatherer", - const std::string& route = "/", const std::string& host = "0.0.0.0", - int port = 3000); + const std::string& route = "/info_gatherer", + const std::string& host = "0.0.0.0", int port = 3000); InfoGathererAgent& set_questions(const std::vector& questions); InfoGathererAgent& set_completion_message(const std::string& msg); InfoGathererAgent& set_prefix(const std::string& prefix); - /// Per-request question producer (dynamic mode). Mirrors the Python - /// callback signature (query_params, body_params, headers) -> questions. + /// Per-request question producer (dynamic mode): + /// (query_params, body_params, headers) -> questions. using QuestionCallback = std::function( const json& query_params, const json& body_params, const json& headers)>; @@ -37,8 +37,26 @@ class InfoGathererAgent : public agent::AgentBase { /// Dynamic-config hook: in static mode returns null (no override); in /// dynamic mode invokes the question callback (or a name/message fallback) /// and returns a {"global_data": {questions, question_index, answers}} - /// override object. Mirrors Python InfoGathererAgent.on_swml_request. - json on_swml_request(const json& request_data, const json& query_params, const json& headers); + /// override object. All three parameters are optional. + /// + /// @param request_data The parsed POST body; treated as an empty object when + /// absent. + /// @param callback_path Optional callback path — unused here, but part of + /// the hook's contract. + /// @param request The request object ``query_params`` and ``headers`` are + /// read off. Modelled as a JSON object with those two keys; when it is + /// absent or not an object, both are treated as empty maps. + /// + /// This is a genuine `override` of swml::Service::on_swml_request. It used to + /// take `const json&` and return `json`, which matched neither the base's + /// arity nor its return type, so it HID the base overload instead of + /// overriding it and the virtual dispatch in src/swml/service.cpp never + /// reached it. Returning std::nullopt is the "no override" answer (it was + /// previously a null `json`). + std::optional on_swml_request( + const std::optional& request_data = std::nullopt, + const std::optional& callback_path = std::nullopt, + const std::optional& request = std::nullopt) override; /// SWAIG tool handler: return the first question. Reads /// questions/question_index from global_data (in raw_data). @@ -57,40 +75,37 @@ class InfoGathererAgent : public agent::AgentBase { /// Typed surveys with validation class SurveyAgent : public agent::AgentBase { public: - explicit SurveyAgent(const std::string& name = "survey", const std::string& route = "/", + explicit SurveyAgent(const std::string& name = "survey", const std::string& route = "/survey", const std::string& host = "0.0.0.0", int port = 3000); SurveyAgent& set_questions(const std::vector& questions); SurveyAgent& set_completion_message(const std::string& msg); SurveyAgent& set_intro_message(const std::string& msg); - /// The survey's display name (reference: ``self.survey_name``), used in the - /// default introduction. + /// The survey's display name, used in the default introduction. SurveyAgent& set_survey_name(const std::string& name); - /// The brand/company name (reference: ``self.brand_name``, default - /// "Our Company"). + /// The brand/company name. Defaults to "Our Company". SurveyAgent& set_brand_name(const std::string& name); - /// Maximum retries for an invalid answer (reference: ``self.max_retries``, - /// default 2). + /// Maximum retries for an invalid answer. Defaults to 2. SurveyAgent& set_max_retries(int retries); - // Configuration the reference keeps as public instance attributes; a caller - // supplies each of these, so a caller reads each back. - /// reference: ``self.survey_name`` + // Configuration accessors: a caller supplies each of these, so a caller + // reads each back. + /// The survey's display name. [[nodiscard]] const std::string& survey_name() const { return survey_name_; } - /// reference: ``self.questions`` — the question objects driving the survey. + /// The question objects driving the survey. [[nodiscard]] const std::vector& questions() const { return survey_questions_; } - /// reference: ``self.brand_name`` + /// The brand/company name. [[nodiscard]] const std::string& brand_name() const { return brand_name_; } - /// reference: ``self.introduction`` — the opening line; defaults to + /// The opening line; defaults to /// "Welcome to our . We appreciate your participation." [[nodiscard]] const std::string& introduction() const { return introduction_; } - /// reference: ``self.conclusion`` — the closing line; defaults to + /// The closing line; defaults to /// "Thank you for completing our survey. Your feedback is valuable to us." [[nodiscard]] const std::string& conclusion() const { return conclusion_; } - /// reference: ``self.max_retries`` + /// Maximum retries for an invalid answer. [[nodiscard]] int max_retries() const { return max_retries_; } - /// Register a post-prompt summary callback (Python SurveyAgent.on_summary). + /// Register a post-prompt summary callback. /// Wires through to AgentBase::on_summary. SurveyAgent& on_summary(agent::SummaryCallback cb); @@ -117,15 +132,14 @@ class SurveyAgent : public agent::AgentBase { class ReceptionistAgent : public agent::AgentBase { public: explicit ReceptionistAgent(const std::string& name = "receptionist", - const std::string& route = "/", const std::string& host = "0.0.0.0", - int port = 3000); + const std::string& route = "/receptionist", + const std::string& host = "0.0.0.0", int port = 3000); ReceptionistAgent& set_departments(const json& departments); ReceptionistAgent& set_greeting(const std::string& greeting); ReceptionistAgent& set_transfer_message(const std::string& msg); - /// Register a post-prompt summary callback (Python - /// ReceptionistAgent.on_summary override point). Wires through to + /// Register a post-prompt summary callback. Wires through to /// AgentBase::on_summary. ReceptionistAgent& on_summary(agent::SummaryCallback cb); }; @@ -133,26 +147,26 @@ class ReceptionistAgent : public agent::AgentBase { /// Keyword-based FAQ matching class FAQBotAgent : public agent::AgentBase { public: - explicit FAQBotAgent(const std::string& name = "faq_bot", const std::string& route = "/", + explicit FAQBotAgent(const std::string& name = "faq_bot", const std::string& route = "/faq", const std::string& host = "0.0.0.0", int port = 3000); FAQBotAgent& set_faqs(const std::vector& faqs); FAQBotAgent& set_no_match_message(const std::string& msg); FAQBotAgent& set_suggest_related(bool suggest); - /// The bot's personality description (reference: ``self.persona``). + /// The bot's personality description. FAQBotAgent& set_persona(const std::string& persona); - // Configuration the reference keeps as public instance attributes. - /// reference: ``self.faqs`` — the FAQ items ({question, answer, categories}). + // Configuration accessors. + /// The FAQ items ({question, answer, categories}). [[nodiscard]] const std::vector& faqs() const { return faqs_; } - /// reference: ``self.suggest_related`` — whether related questions are - /// suggested alongside an answer (default true). + /// Whether related questions are suggested alongside an answer + /// (default true). [[nodiscard]] bool suggest_related() const { return suggest_related_; } - /// reference: ``self.persona`` — defaults to "You are a helpful FAQ bot that - /// provides accurate answers to common questions." + /// The bot's personality description; defaults to "You are a helpful FAQ bot + /// that provides accurate answers to common questions." [[nodiscard]] const std::string& persona() const { return persona_; } - /// Register a post-prompt summary callback (Python FAQBotAgent.on_summary). + /// Register a post-prompt summary callback. /// Wires through to AgentBase::on_summary. FAQBotAgent& on_summary(agent::SummaryCallback cb); @@ -171,30 +185,28 @@ class FAQBotAgent : public agent::AgentBase { /// Venue concierge with amenity info class ConciergeAgent : public agent::AgentBase { public: - explicit ConciergeAgent(const std::string& name = "concierge", const std::string& route = "/", + explicit ConciergeAgent(const std::string& name = "concierge", + const std::string& route = "/concierge", const std::string& host = "0.0.0.0", int port = 3000); ConciergeAgent& set_venue_name(const std::string& name); ConciergeAgent& set_amenities(const std::vector& amenities); ConciergeAgent& set_hours(const json& hours); - /// The services the venue offers (reference: ``self.services``). + /// The services the venue offers. ConciergeAgent& set_services(const std::vector& services); - /// Extra guidance folded into the prompt (reference: - /// ``self.special_instructions``). + /// Extra guidance folded into the prompt. ConciergeAgent& set_special_instructions(const std::vector& instructions); - // Configuration the reference keeps as public instance attributes. - /// reference: ``self.venue_name`` + // Configuration accessors. + /// The venue's display name. [[nodiscard]] const std::string& venue_name() const { return venue_name_; } - /// reference: ``self.amenities`` — the amenity objects ({name, description, - /// location, …}). + /// The amenity objects ({name, description, location, …}). [[nodiscard]] const std::vector& amenities() const { return amenities_; } - /// reference: ``self.services`` + /// The services the venue offers. [[nodiscard]] const std::vector& services() const { return services_; } - /// reference: ``self.hours_of_operation`` — day -> hours; defaults to - /// ``{"default": "9 AM - 5 PM"}``. + /// Day -> hours; defaults to ``{"default": "9 AM - 5 PM"}``. [[nodiscard]] const json& hours_of_operation() const { return hours_of_operation_; } - /// reference: ``self.special_instructions`` + /// Extra guidance folded into the prompt. [[nodiscard]] const std::vector& special_instructions() const { return special_instructions_; } @@ -209,7 +221,7 @@ class ConciergeAgent : public agent::AgentBase { /// otherwise points the guest at the front desk. swaig::FunctionResult get_directions(const json& args, const json& raw_data); - /// Register a post-prompt summary callback (Python ConciergeAgent.on_summary). + /// Register a post-prompt summary callback. /// Wires through to AgentBase::on_summary. ConciergeAgent& on_summary(agent::SummaryCallback cb); diff --git a/include/signalwire/relay/action.hpp b/include/signalwire/relay/action.hpp index 42e2d71..7ec8d4b 100644 --- a/include/signalwire/relay/action.hpp +++ b/include/signalwire/relay/action.hpp @@ -34,14 +34,13 @@ class Action { const std::string& control_id() const { return state_->control_id; } const std::string& state() const; [[nodiscard]] bool completed() const; - /// Corresponds to ``Action.is_done`` — whether the action has finished. + /// Whether the action has finished. Alias of ``completed()``. [[nodiscard]] bool is_done() const { return completed(); } const json& result() const; const std::string& call_id() const { return state_->call_id; } const std::string& node_id() const { return state_->node_id; } - /// The Call this action runs on (reference: ``Action.__init__(call, …)`` - /// stores ``self.call``). The port's Action carries the client + call_id + /// The Call this action runs on. `Action` carries the client + call_id /// rather than a Call reference — because the client's registry OWNS the /// Call and an Action outliving a raw Call& would dangle — so the /// back-reference is resolved through that registry. ``nullptr`` when the @@ -78,15 +77,14 @@ class Action { } /// Detect actions resolve on the first event carrying a `detect` - /// payload, not on a state(finished) — see Python's DetectAction. - /// When this flag is set the action's update_state path resolves - /// only when `params.detect` is present. + /// payload, not on a state(finished). When this flag is set the action's + /// update_state path resolves only when `params.detect` is present. void set_resolve_on_detect(bool flag) { state_->resolve_on_detect = flag; } bool resolve_on_detect() const { return state_->resolve_on_detect; } /// Collect actions resolve when an event carries a `result` payload. /// A play(finished) earlier in the timeline does NOT resolve a - /// CollectAction — see Python's CollectAction terminal-event logic. + /// CollectAction. void set_resolve_on_result(bool flag) { state_->resolve_on_result = flag; } bool resolve_on_result() const { return state_->resolve_on_result; } @@ -103,8 +101,7 @@ class Action { /// Request the server to pause this action. The optional `behavior` /// (e.g. "continuous" for record-side pause) is sent as the `behavior` - /// frame field only when provided — matching Python's - /// `pause(behavior: str | None = None)`. + /// frame field only when provided; otherwise the field is omitted. void pause(const std::optional& behavior = std::nullopt); /// Request the server to resume this action. @@ -114,8 +111,7 @@ class Action { /// supplied amount in dB; positive boosts, negative attenuates. void volume(double amount); - /// Start the inter-digit / final-digit timers on a collect. The - /// matching Python method is StandaloneCollectAction.start_input_timers. + /// Start the inter-digit / final-digit timers on a collect. void start_input_timers(); /// Set a callback to fire when the action completes. @@ -131,6 +127,26 @@ class Action { void send_control_command(const std::string& operation, const json& extra_params = json::object()); + /// The state an `Action` and all of its copies share. + /// + /// `Action` is a value type callers pass around and store by copy, but every + /// copy must observe the SAME completion — so the real state lives here + /// behind a `shared_ptr` and copying an `Action` shares it rather than + /// duplicating it. Resolving the copy the Call registry holds therefore + /// resolves the copy on the caller's stack. + /// + /// The first group identifies the action on the wire: `control_id` correlates + /// server events back to it, `call_id`/`node_id` address the leg, and + /// `method_prefix` (default `calling.play`) determines which RPC the control + /// commands send — a `record()` action carries `calling.record` so `stop()` + /// emits `calling.record.stop`. `event_type_filter`, `resolve_on_detect`, and + /// `resolve_on_result` encode the verb-specific completion semantics on the + /// unified base rather than in per-verb subclasses. `client` is a + /// NON-OWNING back-pointer used to send those frames. + /// + /// The second group is the completion rendezvous: `mutex` guards + /// `current_state`, `is_completed`, `result_data`, and `completed_callback`, + /// and `cv` wakes threads blocked waiting for the action to finish. struct SharedState { std::string control_id; std::string call_id; diff --git a/include/signalwire/relay/call.hpp b/include/signalwire/relay/call.hpp index 55fda31..c12b908 100644 --- a/include/signalwire/relay/call.hpp +++ b/include/signalwire/relay/call.hpp @@ -53,18 +53,17 @@ class Call { const std::string& from() const { return s_->from; } const std::string& to() const { return s_->to; } const std::string& tag() const { return s_->tag; } - /// reference: ``self.project_id`` — the SignalWire project the call belongs - /// to (from the inbound event's ``project_id``, else the client's project). + /// The SignalWire project the call belongs to (from the inbound event's + /// ``project_id``, else the client's project). const std::string& project_id() const { return s_->project_id; } - /// reference: ``self.context`` — the RELAY context the call arrived on - /// (the connect-issued protocol, else the event's ``context``/``protocol``). + /// The RELAY context the call arrived on (the connect-issued protocol, else + /// the event's ``context``/``protocol``). const std::string& context() const { return s_->context; } - /// reference: ``self.segment_id`` — the call segment identifier, empty when - /// the server did not report one. + /// The call segment identifier, empty when the server did not report one. const std::string& segment_id() const { return s_->segment_id; } - /// reference: ``self.device`` — the raw device descriptor object from the - /// inbound event (``{type, params:{from_number,to_number,…}}``); an empty - /// object when absent, matching the reference's ``device or {}``. + /// The raw device descriptor object from the inbound event + /// (``{type, params:{from_number,to_number,…}}``); an empty object when + /// absent. const json& device() const { return s_->device; } bool is_answered() const { return s_->state == CALL_STATE_ANSWERED; } @@ -95,8 +94,6 @@ class Call { Action play_ringtone(const std::string& name, double duration = -1.0, double volume = 0.0); Action record(const json& params = json::object(), const std::string& control_id = ""); Action record_call(const json& params = json::object()); - Action prompt(const json& play_media, const json& collect_params, - const std::string& control_id = ""); Action play_and_collect(const json& play_media, const json& collect_params, const std::string& control_id = ""); // Typed prompt convenience wrappers (mirror Python's prompt_tts/ @@ -116,10 +113,8 @@ class Call { /// Bridge the call to one or more destinations. ``devices`` is the nested /// serial/parallel device array; ``options`` carries the optional bridge /// knobs (``ringback``, ``tag``, ``max_duration``, ``max_price_per_minute``, - /// ``status_url``, or any extra) merged into the ``calling.connect`` frame — - /// Corresponds to ``Call.connect(devices, *, ringback=…, tag=…, - /// max_duration=…, **kwargs)``. Without ``options`` these knobs never reached - /// the wire. + /// ``status_url``, or any extra) merged into the ``calling.connect`` frame. + /// Without ``options`` these knobs never reach the wire. Action connect(const json& devices, const json& options = json::object()); Action disconnect(); Action detect(const json& params, const std::string& control_id = ""); @@ -182,8 +177,17 @@ class Call { /// Disable denoise on the call (calling.denoise.stop). Action denoise_stop(); /// Bind a digit sequence to a method (calling.bind_digit). + /// + /// ``bind_params`` is emitted under the nested WIRE key ``params`` + /// (``params["params"] = bind_params``). Every other knob this method offers + /// is spelled identically on the API and the wire, so the trailing options + /// bag carries them verbatim; ``bind_params`` is the one that needs its own + /// parameter, because a caller who put it in the bag would ship it under the + /// wrong wire key. Bag keys still ride through. The bag stays in its existing + /// 3rd position so the current call shape keeps its meaning. Action bind_digit(const std::string& digits, const std::string& bind_method, - const json& params = json::object()); + const json& params = json::object(), + const std::optional& bind_params = std::nullopt); /// Clear digit bindings, optionally scoped to a realm /// (calling.clear_digit_bindings). Action clear_digit_bindings(const std::string& realm = ""); @@ -192,36 +196,41 @@ class Call { /// Leave a queue (calling.queue.leave). Action queue_leave(const std::string& queue_name, const json& params = json::object()); /// Leave the current conference (calling.leave_conference). - Action leave_conference(const std::string& conference_id = ""); + /// ``conference_id`` is REQUIRED — it has no default and is always sent. + Action leave_conference(const std::string& conference_id); /// Leave the current room (calling.leave_room). Action leave_room(); /// AI helpers. Action ai_hold(const json& params = json::object()); Action ai_unhold(const json& params = json::object()); Action ai_message(const json& params = json::object()); - /// Start Amazon Bedrock AI on the call. RULES §4: calling.ai + a Bedrock - /// engine routes to a DEDICATED `calling.amazon_bedrock` RPC, so this - /// emits that wire method rather than `calling.ai`. - Action amazon_bedrock(const json& params = json::object()); - /// Pass on an inbound call offer (calling.pass). Named `pass_` because - /// `pass` is not a C++ keyword but the reserved-word rename convention is - /// applied for cross-language consistency (wire method stays `pass`). + /// Start Amazon Bedrock AI on the call. A Bedrock engine routes to a + /// DEDICATED `calling.amazon_bedrock` RPC, so this emits that wire method + /// rather than `calling.ai`. + /// + /// ``ai_params`` is emitted under the nested WIRE key ``params`` + /// (``params["params"] = ai_params``); same reason as + /// ``bind_digit(bind_params)``. Every other knob is spelled the same on the + /// API and the wire and rides in the leading bag, which stays FIRST so the + /// existing single-argument call shape keeps its meaning. + Action amazon_bedrock(const json& params = json::object(), + const std::optional& ai_params = std::nullopt); + /// Pass on an inbound call offer (calling.pass). Named `pass_` by the + /// SDK-wide reserved-word rename convention; the wire method stays `pass`. Action pass_(); // Event handling void on_event(CallEventHandler handler); - /// Register an event handler (Python/Java `on`). Alias of on_event — the - /// unified name the reference exposes. + /// Register an event handler. Alias of on_event. void on(CallEventHandler handler) { on_event(std::move(handler)); } /// Block until the call reaches `target_state` (one of the /// CALL_STATE_* values), returning true on reaching it (or already at/past - /// it) and false on timeout. Python/Java `wait_for`. Backed by the same - /// lifecycle-rank machinery as wait_for_answered/ringing/ending. + /// it) and false on timeout. Backed by the same lifecycle-rank machinery as + /// wait_for_answered/ringing/ending. /// [[nodiscard]] for the same reason as those: ignoring reached-vs-timeout /// is always a bug. [[nodiscard]] bool wait_for(const std::string& target_state, int timeout_ms = 0); - /// Python `__repr__` — a compact debug string `Call(id=..., state=..., - /// direction=...)`. Named `repr()` (the reserved-name rename of the dunder). + /// A compact debug string `Call(id=..., state=..., direction=...)`. [[nodiscard]] std::string repr() const; // [[nodiscard]]: the return value is the whole point of a wait — it tells // you whether the call actually reached the terminal state vs. timed out. @@ -277,6 +286,29 @@ class Call { // collect-only resolution. Backs prompt_tts/prompt_audio. Action prompt_with_media(const json& media, const json& collect, double volume); + /// The state a `Call` and all of its copies share. + /// + /// Like `Action`, `Call` is a copyable value whose copies must observe the + /// same leg, so the real state sits behind a `shared_ptr`. + /// + /// It holds the leg's wire identity and metadata (`call_id`/`node_id`, + /// `state`, `direction`, `from`/`to`, `tag`, plus `project_id`, + /// `context`, `segment_id`, and `device`), a NON-OWNING `client` back-pointer for + /// sending frames, and three pieces of concurrency machinery: + /// + /// * `event_handlers` + `handlers_mutex` — `on_event()` mutates the vector + /// from the user thread while `dispatch_event()` iterates it from the + /// WebSocket reader thread, so the mutex is load-bearing, not defensive. + /// * `actions` + `actions_mutex` — the in-flight action registry, keyed by + /// `control_id`. It stores `Action` BY VALUE: an `Action` keeps its state + /// in a `shared_ptr`, so the stored copy shares state with the caller's + /// copy and resolving one resolves both. Storing a raw `Action*` dangled + /// as soon as the caller's stack-local went out of scope. + /// * `ended_mutex` + `ended_cv` + `state_cv` — the wait rendezvous. + /// `state_cv` is notified on EVERY state transition, not only on `ended`, + /// which is what lets `wait_for_answered`/`ringing`/`ending` wake on + /// intermediate states; both condition variables are guarded by + /// `ended_mutex`, which already serialises `state`. struct SharedState { std::string call_id; std::string node_id; diff --git a/include/signalwire/relay/client.hpp b/include/signalwire/relay/client.hpp index c2b02c8..6504fa8 100644 --- a/include/signalwire/relay/client.hpp +++ b/include/signalwire/relay/client.hpp @@ -28,7 +28,7 @@ namespace relay { using json = nlohmann::json; -/// Error returned by the RELAY server (Python: ``relay.client.RelayError``). +/// Error returned by the RELAY server. /// Carries the server-supplied JSON-RPC error ``code`` + ``message``. class RelayError : public std::runtime_error { public: @@ -63,8 +63,8 @@ struct RelayConfig { std::string token; /// JWT bearer credential — the alternative to project/token. When set, the /// connect frame authenticates with ``{"jwt_token": …}`` and project/token - /// are not required (the project id is inside the token). Mirrors the - /// reference's ``RelayClient(jwt_token=…)`` / ``SIGNALWIRE_JWT_TOKEN``. + /// are not required (the project id is inside the token). Also settable via + /// the ``SIGNALWIRE_JWT_TOKEN`` environment variable. std::string jwt_token; std::string host = DEFAULT_HOST; int port = DEFAULT_PORT; @@ -111,19 +111,24 @@ class RelayClient { void on_call(InboundCallHandler handler); /// Dial outbound. The `devices` argument is the nested - /// "device-of-leg-of-leg" array used by the Python SDK - /// (`[[{type:phone,...}]]`). Returns a Call once the server emits - /// calling.call.dial(answered) for the dial's tag, or an empty Call - /// on timeout / failure. + /// "device-of-leg-of-leg" array (`[[{type:phone,...}]]`). Returns a Call once + /// the server emits calling.call.dial(answered) for the dial's tag, or an + /// empty Call on timeout / failure. + /// + /// PARAMETER ORDER CHANGED: this previously read + /// ``(devices, tag, dial_timeout_ms, max_duration)``, so a positional third + /// argument now means something different. Callers passing a positional + /// 3rd/4th argument must swap them. /// /// `tag` lets callers pin an explicit dial tag for journal-based /// assertions; if blank, a UUID is generated. - /// `dial_timeout_ms` caps how long dial() blocks waiting for the - /// server's terminal dial event. - /// `max_duration` (seconds) is forwarded into the calling.dial frame - /// when non-zero. - Call dial(const json& devices, const std::string& tag = "", int dial_timeout_ms = 120000, - int max_duration = 0); + /// `max_duration` is the max call duration in MINUTES, forwarded into the + /// calling.dial frame when non-zero. + /// `dial_timeout` is how long, in SECONDS, dial() blocks waiting for the + /// server's terminal dial event. ABSENT by default; the body substitutes + /// 120s. Note the UNIT: this used to be `dial_timeout_ms` in milliseconds. + Call dial(const json& devices, const std::string& tag = "", int max_duration = 0, + std::optional dial_timeout = std::nullopt); /// Register a generic event observer. Called for every dispatched /// `signalwire.event` after typed routing (on_call/on_message/action @@ -152,17 +157,15 @@ class RelayClient { void subscribe(const std::vector& contexts); void unsubscribe(const std::vector& contexts); - /// Subscribe to additional contexts for inbound events (Python: - /// ``RelayClient.receive``). Sends ``signalwire.receive`` on the assigned - /// protocol so inbound calls on ``contexts`` start being delivered; can be - /// called after ``connect()`` to add contexts without reconnecting. Thin - /// Python-named alias of ``subscribe``. + /// Subscribe to additional contexts for inbound events. Sends + /// ``signalwire.receive`` on the assigned protocol so inbound calls on + /// ``contexts`` start being delivered; can be called after ``connect()`` to + /// add contexts without reconnecting. Thin alias of ``subscribe``. void receive(const std::vector& contexts) { subscribe(contexts); } - /// Unsubscribe from contexts for inbound events (Python: - /// ``RelayClient.unreceive``). Sends ``signalwire.unreceive`` to stop - /// receiving inbound calls on ``contexts``. Thin Python-named alias of - /// ``unsubscribe``. + /// Unsubscribe from contexts for inbound events. Sends + /// ``signalwire.unreceive`` to stop receiving inbound calls on ``contexts``. + /// Thin alias of ``unsubscribe``. void unreceive(const std::vector& contexts) { unsubscribe(contexts); } // Accessors @@ -173,26 +176,25 @@ class RelayClient { // (`self.project` / `self.token` / `self.jwt_token` / `self.host` / // `self.contexts`). The port stores them in `config_`; these read them back // under the reference's flat names. - /// reference: ``self.project`` — the SignalWire project id (empty under JWT auth). + /// The SignalWire project id (empty under JWT auth). const std::string& project() const { return config_.project; } - /// reference: ``self.token`` — the API token (unused under JWT auth). + /// The API token (unused under JWT auth). const std::string& token() const { return config_.token; } - /// reference: ``self.jwt_token`` — the JWT credential; when non-empty the - /// connect frame authenticates with it instead of project/token. + /// The JWT credential; when non-empty the connect frame authenticates with + /// it instead of project/token. const std::string& jwt_token() const { return config_.jwt_token; } - /// reference: ``self.host`` — the RELAY host (a bare hostname, not a URL). + /// The RELAY host (a bare hostname, not a URL). const std::string& host() const { return config_.host; } - /// reference: ``self.contexts`` — the contexts subscribed at connect. + /// The contexts subscribed at connect. const std::vector& contexts() const { return config_.contexts; } /// Server-assigned session id captured from the `signalwire.connect` /// handshake result (`result.sessionid`). Empty until a successful /// connect. Production code never needs this — it exists so the test /// harness can scope the mock's journal/scenarios/pushes to this client's - /// session and run safely under parallel execution. Python's RelayClient - /// keeps the equivalent internal too; exposing a read-only accessor here - /// (rather than a bare public field) keeps it off the mutable surface. - /// Documented in PORT_ADDITIONS.md as cpp_relay_session_id_accessor. + /// session and run safely under parallel execution. Exposed as a read-only + /// accessor rather than a bare public field to keep it off the mutable + /// surface. const std::string& session_id() const { return session_id_; } // JSON-RPC execution (used by Call and Action objects) @@ -265,6 +267,16 @@ class RelayClient { std::unique_ptr ws_; // Correlation mechanism 1: JSON-RPC id -> promise + /// One in-flight JSON-RPC request awaiting its response frame. + /// + /// The sending thread parks on this promise's future while the WebSocket + /// reader thread matches an inbound frame's `id` back to this entry and + /// fulfils the promise with the result. Held by `shared_ptr` in + /// `pending_requests_` so the entry stays alive even if the map is cleared + /// while a waiter still holds it. On disconnect, `reject_all_pending` fulfils + /// every outstanding promise with a `{code:"503", message:"Connection lost"}` + /// result rather than leaving it unsatisfied — an abandoned promise would + /// block its waiter forever. struct PendingRequest { std::promise promise; }; @@ -278,6 +290,15 @@ class RelayClient { // Correlation mechanism 3: control_id -> Action (tracked per Call) // Correlation mechanism 4: tag -> promise for dials + /// One in-flight outbound dial awaiting the call it creates. + /// + /// A dial cannot be correlated by JSON-RPC id: the `Call` does not exist + /// until the server reports it, so the dial is keyed by the caller-generated + /// `tag` and the promise is fulfilled with the owned `Call*` when an event + /// bearing that tag arrives. Held by `shared_ptr` in `pending_dials_` for the + /// same lifetime reason as `PendingRequest`. On disconnect, + /// `reject_all_pending` fulfils it with `nullptr`, so a waiting caller gets a + /// null Call rather than hanging. struct PendingDial { std::promise promise; }; diff --git a/include/signalwire/relay/device.hpp b/include/signalwire/relay/device.hpp index e26682d..c80b2a1 100644 --- a/include/signalwire/relay/device.hpp +++ b/include/signalwire/relay/device.hpp @@ -10,25 +10,22 @@ namespace relay { using json = nlohmann::json; -// =========================================================================== -// Device — the {type, params} object passed as a raw json map across the RELAY -// calling methods that target an endpoint: connect / refer / dial / tap. -// -// Grounded in the RELAY calling protocol schema for dial/connect/refer: each -// device is an object -// with a REQUIRED string `type` discriminant (e.g. "phone", "sip", "webrtc") -// and an open `params` object whose keys depend on `type`. The schema is -// `additionalProperties:true` and does NOT enumerate the `type` values, so we -// type the SHAPE only: `type` stays a `std::string` (open discriminant), and -// `params` stays a free `json` map. `to_json()` yields the IDENTICAL wire shape -// the hand-written `{{"type",...},{"params",...}}` map produces. -// -// Additive idiom (PORT_ADDITIONS.md): the raw-`json` connect/dial/refer/tap -// overloads stay canonical (matches Python's nested dict/list). `Device` -// is a typed convenience for assembling that map with a named field instead of -// stringly keys — `Device{"phone", {{"to_number", to}}}` reads better than the -// brace-soup and can't typo the two top-level keys. -// =========================================================================== +/// The `{type, params}` endpoint object the RELAY calling methods that target +/// an endpoint take: connect / refer / dial / tap. +/// +/// Grounded in the RELAY calling protocol schema for dial/connect/refer: each +/// device is an object with a REQUIRED string `type` discriminant (e.g. +/// "phone", "sip", "webrtc") and an open `params` object whose keys depend on +/// `type`. The schema is `additionalProperties:true` and does NOT enumerate the +/// `type` values, so this types the SHAPE only: `type` stays a `std::string` +/// (open discriminant), and `params` stays a free `json` map. `to_json()` +/// yields the IDENTICAL wire shape the hand-written +/// `{{"type",...},{"params",...}}` map produces. +/// +/// The raw-`json` connect/dial/refer/tap overloads stay canonical. `Device` is a +/// typed convenience for assembling that map with a named field instead of +/// stringly keys — `Device{"phone", {{"to_number", to}}}` reads better than the +/// brace-soup and can't typo the two top-level keys. struct Device { /// REQUIRED endpoint-type discriminant. Open set (not schema-enumerated) → /// kept a std::string. Common values: "phone", "sip", "webrtc". diff --git a/include/signalwire/relay/message.hpp b/include/signalwire/relay/message.hpp index 30a3a2c..54dce03 100644 --- a/include/signalwire/relay/message.hpp +++ b/include/signalwire/relay/message.hpp @@ -37,23 +37,21 @@ struct Message { // Identity / outbound metadata. These are write-once-by-construction so // sharing across copies isn't required for these fields. std::string message_id; - /// Wire keys ``from_number`` / ``to_number`` (reference: ``self.from_number`` - /// / ``self.to_number``). Neither is a C++ reserved word, so the field carries - /// the reference's spelling — the shorter ``from``/``to`` was a gratuitous - /// divergence from the wire key it is read from. + /// Named for the wire keys ``from_number`` / ``to_number`` they are read + /// from. The shorter ``from``/``to`` would be a gratuitous divergence from + /// those keys (and neither name is a C++ reserved word, so nothing forces it). std::string from_number; std::string to_number; std::string body; std::vector media; std::vector tags; std::string direction; - /// Messaging context this message belongs to (reference: ``self.context``). - /// Set from the ``context`` key on an inbound ``messaging.receive`` event and - /// from the requested context on an outbound send. + /// Messaging context this message belongs to. Set from the ``context`` key on + /// an inbound ``messaging.receive`` event and from the requested context on + /// an outbound send. std::string context; - /// Number of SMS segments the carrier split this message into (reference: - /// ``self.segments``); populated from the inbound event's ``segments`` key, - /// 0 when the server did not report one. + /// Number of SMS segments the carrier split this message into; populated from + /// the inbound event's ``segments`` key, 0 when the server did not report one. int segments = 0; std::string region; @@ -94,24 +92,38 @@ struct Message { // ---- Public surface (signalwire.relay.message.Message) ---------- - /// Whether the message has reached a terminal state (Python: ``is_done``). + /// Whether the message has reached a terminal state. Alias of is_terminal. [[nodiscard]] bool is_done() const { return is_terminal(); } - /// Register a terminal-state callback (Python: ``on``). Alias of on_completed. + /// Register a terminal-state callback. Alias of on_completed. void on(CompletedCallback cb) { on_completed(std::move(cb)); } - /// Terminal outcome as a JSON object (Python: ``result``): the final state, - /// reason, and message id. + /// Terminal outcome as a JSON object: the final state, reason, and + /// message id. [[nodiscard]] json result() const { return json::object({{"message_id", message_id}, {"state", state()}, {"reason", reason()}}); } - /// String representation (Python: ``__repr__``). + /// Compact debug string `Message(id='...', state='...')`. [[nodiscard]] std::string repr() const { return "Message(id='" + message_id + "', state='" + state() + "')"; } private: + /// The delivery state a `Message` and all of its copies share. + /// + /// `Message` is copied and returned by value, but a copy must observe the + /// same delivery outcome as the instance the client registry tracks — so the + /// mutable half lives here behind a `shared_ptr` while the write-once + /// identity fields (`message_id`, `from_number`, …) stay on the Message + /// itself. + /// + /// `mutex` guards all four members. `state` and `reason` are overwritten by + /// `update_state` as `messaging.state` events arrive from the WebSocket + /// reader thread; `completed` latches once a terminal state is reached, at + /// which point `cv` wakes every thread blocked in `wait()` and `callback` + /// (set via `on_completed`/`on`) is invoked. Registering a callback on an + /// already-terminal message fires it immediately. struct SyncState { std::mutex mutex; std::condition_variable cv; diff --git a/include/signalwire/relay/states.hpp b/include/signalwire/relay/states.hpp index a991676..057b829 100644 --- a/include/signalwire/relay/states.hpp +++ b/include/signalwire/relay/states.hpp @@ -53,8 +53,8 @@ namespace relay { // CallState — calling.call.state `call_state` // --------------------------------------------------------------------------- -/// Call lifecycle state. Mirrors `CALL_STATE_*` / `CALL_STATES` in -/// `relay/constants.py`. Server-emitted and may grow → `call_state_from_string` +/// Call lifecycle state. The typed form of `CALL_STATE_*` / `CALL_STATES` in +/// `relay/constants.hpp`. Server-emitted and may grow → `call_state_from_string` /// returns an optional on an unknown value. enum class CallState { Created, @@ -159,7 +159,8 @@ enum class DialState { // MessageState — messaging.state `message_state` // --------------------------------------------------------------------------- -/// SMS/MMS delivery state. Mirrors `MESSAGE_STATE_*` in `relay/constants.py`. +/// SMS/MMS delivery state. The typed form of `MESSAGE_STATE_*` in +/// `relay/constants.hpp`. /// Terminal set == `MESSAGE_TERMINAL_STATES` {delivered, undelivered, failed}. /// NOTE `failed` here is the MESSAGE failure state — NOT `DialState::Failed`; /// the two vocabularies are separate and must not be unified. Server-emitted → diff --git a/include/signalwire/relay/tts_gender.hpp b/include/signalwire/relay/tts_gender.hpp index e089072..46e887c 100644 --- a/include/signalwire/relay/tts_gender.hpp +++ b/include/signalwire/relay/tts_gender.hpp @@ -13,9 +13,8 @@ namespace relay { /// `std::string` for their `gender` argument. The enum gives editor /// autocompletion and makes a typo fail at the call site — a bare string /// like `"femaie"` only fails at runtime, on the TTS engine. The string -/// overload matches the Python reference (which uses a bare -/// `Optional[str]`) and still allows engine-/voice-specific values that -/// aren't one of the two canonical genders. +/// overload keeps the set open, so engine-/voice-specific values that aren't +/// one of the two canonical genders stay expressible. /// /// call.play_tts("hi", "en-US", Gender::Female); // typed, autocompleted /// call.play_tts("hi", "en-US", "female"); // string still works diff --git a/include/signalwire/relay/typed_events.hpp b/include/signalwire/relay/typed_events.hpp index d40edc2..fb4c6e1 100644 --- a/include/signalwire/relay/typed_events.hpp +++ b/include/signalwire/relay/typed_events.hpp @@ -145,11 +145,10 @@ struct CollectEvent : public RelayEvent { std::string control_id; std::string state; /// The collect result is a structured object (e.g. - /// ``{"type":"digit","params":{"digits":"1234"}}``), not a scalar — the - /// reference records ``result: dict``. Reading it as a string drops the payload. + /// ``{"type":"digit","params":{"digits":"1234"}}``), not a scalar. + /// Reading it as a string drops the payload. json result = json::object(); - /// Tri-state: absent (nullopt), true, or false — matches Python - /// ``final: bool | None``. + /// Tri-state: absent (nullopt), true, or false. std::optional final; [[nodiscard]] static CollectEvent from_payload(const json& payload) { RelayEvent base = RelayEvent::from_payload(payload); diff --git a/include/signalwire/rest/base_resource.hpp b/include/signalwire/rest/base_resource.hpp index 0dab64e..823d5d5 100644 --- a/include/signalwire/rest/base_resource.hpp +++ b/include/signalwire/rest/base_resource.hpp @@ -10,10 +10,10 @@ #include "signalwire/rest/request_options.hpp" // Hand-written base hierarchy for the GENERATED REST resource layer -// (include/signalwire/rest/namespaces/generated/*.hpp). Mirrors Python's -// signalwire/rest/_base.py: BaseResource (bare receiver), ReadResource -// (list/get), CrudResource (list/create/get/update/delete with a per-resource -// update verb), FabricResource (CrudResource + list_addresses). Each generated +// (include/signalwire/rest/namespaces/generated/*.hpp): BaseResource (bare +// receiver), ReadResource (list/get), CrudResource (list/create/get/update/ +// delete with a per-resource update verb), FabricResource (CrudResource + +// list_addresses). Each generated // resource class EXTENDS one of these and supplies only its resource-specific // operation / command / set_* / sub-collection methods; the shared CRUD verbs // live here so the emitted headers stay thin. @@ -46,7 +46,7 @@ class BaseResource { std::string base_path_; }; -/// Read-only resource: ``list`` + ``get`` (Python ReadResource). +/// Read-only resource: ``list`` + ``get``. class ReadResource : public BaseResource { public: ReadResource(const HttpClient& client, const std::string& base_path) @@ -58,7 +58,7 @@ class ReadResource : public BaseResource { } /// Iterate every item across all pages of this resource's list endpoint. /// - /// Mirrors Python's ``ReadResource.paginate``: ``list()`` returns a single + /// ``list()`` returns a single /// raw page; ``paginate()`` returns a ``PaginatedIterator`` that walks /// ``resp["data"]`` and follows ``resp["links"]["next"]`` so callers page /// through a list endpoint without hand-building the token loop. @@ -78,8 +78,8 @@ class ReadResource : public BaseResource { } }; -/// Full CRUD resource (Python CrudResource). The update verb (PUT vs PATCH) is -/// baked in at construction, mirroring Python's ``_update_method``. +/// Full CRUD resource. The update verb (PUT vs PATCH) is baked in at +/// construction and defaults to PATCH. class CrudResource : public BaseResource { public: CrudResource(const HttpClient& client, const std::string& base_path, @@ -103,8 +103,8 @@ class CrudResource : public BaseResource { return (update_method_ == "PUT") ? client_.put(base_path_ + "/" + id, data, request_options) : client_.patch(base_path_ + "/" + id, data, request_options); } - /// ``delete_`` — ``delete`` is a C++ keyword; the enumerator renames this to - /// the canonical ``delete`` (reserved-word rename, wire verb DELETE preserved). + /// ``delete_`` carries a trailing underscore because ``delete`` is a C++ + /// keyword. The HTTP verb sent is DELETE. [[nodiscard]] json delete_(const std::string& id, const RequestOptions& request_options = {}) const { return client_.del(base_path_ + "/" + id, request_options); @@ -114,11 +114,11 @@ class CrudResource : public BaseResource { std::string update_method_; }; -/// Fabric resource: CRUD + the ``list_addresses`` sub-collection (Python -/// FabricResource / CrudWithAddresses). The base ``list_addresses`` hangs off -/// this resource's own base path; the four sibling-path fabric subclasses -/// (CallFlows / ConferenceRooms / CxmlApplications / GenericResources) override -/// it with their singularised sub-path in the generated header (L12). +/// Fabric resource: CRUD + the ``list_addresses`` sub-collection. The base +/// ``list_addresses`` hangs off this resource's own base path; the four +/// sibling-path fabric subclasses (CallFlows / ConferenceRooms / +/// CxmlApplications / GenericResources) override it with their singularised +/// sub-path in the generated header. class FabricResource : public CrudResource { public: FabricResource(const HttpClient& client, const std::string& base_path, diff --git a/include/signalwire/rest/http_client.hpp b/include/signalwire/rest/http_client.hpp index 8aa9b8f..f5852b6 100644 --- a/include/signalwire/rest/http_client.hpp +++ b/include/signalwire/rest/http_client.hpp @@ -23,10 +23,9 @@ using json = nlohmann::json; /// Error thrown on non-2xx REST API responses. /// /// Carries the full request/response envelope — HTTP ``status`` code, response -/// ``body``, the request ``url`` and ``method`` — mirroring Python's -/// ``SignalWireRestError(status_code, body, url, method, headers)``. Every -/// field is exposed so a caller catching the error can inspect exactly which -/// request failed and how. +/// ``body``, the request ``url`` and ``method``, and the response ``headers``. +/// Every field is exposed so a caller catching the error can inspect exactly +/// which request failed and how. /// /// §6.6 error-observability: ``headers()`` is the response header map (empty /// for a transport error that produced no response) and ``request_id()`` is @@ -39,7 +38,7 @@ class SignalWireRestError : public std::runtime_error { SignalWireRestError(int status, const std::string& message, const std::string& body = "", const std::string& url = "", const std::string& method = "GET", const std::map& headers = {}); - /// HTTP status of the failing response (reference: ``self.status_code``). + /// HTTP status of the failing response. /// ``0`` for a transport failure that never reached a response. int status_code() const { return status_; } const std::string& body() const { return body_; } @@ -50,16 +49,16 @@ class SignalWireRestError : public std::runtime_error { const std::map& headers() const { return headers_; } /// Platform request id extracted from the response headers — the first of /// ``x-request-id`` / ``x-signalwire-request-id`` / ``request-id`` / - /// ``x-amzn-requestid`` present (matched case-insensitively; the same - /// preference order as the Python reference). Empty string when absent. + /// ``x-amzn-requestid`` present, matched case-insensitively and in that + /// preference order. Empty string when absent. const std::string& request_id() const { return request_id_; } private: // Defined in src/rest/http_client.cpp: first matching request-id header - // (case-insensitive, python-reference preference order), else "". + // (case-insensitive, in the documented preference order), else "". static std::string extract_request_id(const std::map& headers); - // Defined in src/rest/http_client.cpp: appends the python-mirrored - // request-id suffix to the message when an id is present. + // Defined in src/rest/http_client.cpp: appends the request-id suffix to the + // message when an id is present. static std::string with_request_id(const std::string& message, const std::string& request_id); int status_; @@ -75,14 +74,12 @@ class SignalWireRestError : public std::runtime_error { /// reset, TLS error), as opposed to a well-formed non-2xx HTTP response. /// /// A member of the ``SignalWireRestError`` family: ``status_code()`` is ``0`` -/// (the sentinel this port uses for "no HTTP status" — there is no response -/// to carry one), and the underlying transport-library error text is +/// (the sentinel for "no HTTP status" — there is no response to carry one), +/// and the underlying transport-library error text is /// preserved as the exception message. Because it extends /// ``SignalWireRestError``, a caller catching that one type handles both an /// HTTP-error response and a transport failure with a single ``catch``, -/// instead of a bare cpp-httplib/curl error leaking through. Mirrors the -/// Python reference's ``SignalWireRestTransportError(SignalWireRestError)`` -/// (plan 1.3b). +/// instead of a bare cpp-httplib/curl error leaking through. class SignalWireRestTransportError : public SignalWireRestError { public: SignalWireRestTransportError(const std::string& message, const std::string& url = "", @@ -130,8 +127,6 @@ class HttpClient { /// Sets the underlying SSLClient's CA path and keeps server-certificate /// verification ON. Production (public CAs) needs no call — the system /// trust store is used, and SSL_CERT_FILE is also honored automatically. - /// C++-only ergonomic hook (Python's requests-based client trusts a custom - /// CA via the SSL_CERT_FILE / REQUESTS_CA_BUNDLE env vars instead). void set_ca_cert_path(const std::string& path); const std::string& base_url() const { return base_url_; } @@ -160,20 +155,19 @@ class HttpClient { /// Iterates items across paginated API responses. /// -/// Mirrors signalwire-python's ``signalwire.rest._pagination.PaginatedIterator``: -/// fetches the configured path with the configured params, walks the +/// Fetches the configured path with the configured params, walks the /// ``data_key`` array, then follows ``links.next`` (parsing its query string /// for the next page's params) until the response carries no ``links.next``. /// /// Iteration is lazy -- the constructor records inputs but performs no /// HTTP. The first ``has_next()`` / ``next()`` call performs the first -/// fetch. Cursor query params are extracted by parsing ``links.next`` like -/// Python's ``urllib.parse.urlparse + parse_qs``. +/// fetch. Cursor query params are extracted by parsing the ``links.next`` URL +/// and decoding its query string. class PaginatedIterator { public: /// ``request_options`` (per-request transport envelope: timeout / retries / - /// abort) is the reference's trailing param — it is applied to every page - /// fetch this iterator performs. Never part of the query/body. + /// abort) is applied to every page fetch this iterator performs. It is never + /// part of the query or body. PaginatedIterator(const HttpClient& http, const std::string& path, const std::map& params = {}, const std::string& data_key = "data", @@ -187,7 +181,7 @@ class PaginatedIterator { [[nodiscard]] bool has_next(); /// Returns the next item; throws std::out_of_range when the iterator - /// is exhausted (mirrors Python's StopIteration). + /// is exhausted. /// [[nodiscard]]: dropping the returned item silently consumes it. [[nodiscard]] json next(); @@ -212,10 +206,10 @@ class PaginatedIterator { std::vector items_; size_t index_ = 0; bool done_ = false; - // Cycle guard (CPP-4): next-link cursors already followed. A server that - // keeps returning the SAME ``links.next`` would otherwise loop forever, since - // termination is now driven only by an ABSENT next link (empty-page fix). - // Seeing a repeat terminates iteration. Mirrors Python's _seen_next. + // Cycle guard: next-link cursors already followed. A server that keeps + // returning the SAME ``links.next`` would otherwise loop forever, since + // termination is driven only by an ABSENT next link. Seeing a repeat + // terminates iteration. std::set seen_next_; }; diff --git a/include/signalwire/rest/request_options.hpp b/include/signalwire/rest/request_options.hpp index 809e382..b7a971f 100644 --- a/include/signalwire/rest/request_options.hpp +++ b/include/signalwire/rest/request_options.hpp @@ -10,12 +10,11 @@ namespace signalwire { namespace rest { -/// RequestOptions — the REST request-options envelope (plan 4.2). +/// RequestOptions — the REST request-options envelope. /// /// A single value object controlling per-request transport behavior: timeout, /// retries (with an idempotency-aware retry policy + exponential backoff), and -/// cooperative cancellation. Mirrors Python's -/// ``signalwire.rest._request_options.RequestOptions``. +/// cooperative cancellation. /// /// Supplied at two levels: /// - **Client default**: ``RestClient(..., request_options)`` stored on the @@ -28,10 +27,10 @@ namespace rest { /// defaults (the contract floor) live on ``HttpClient`` and are resolved at /// apply-time (per-request over client-default over built-in). /// -/// ``abort_signal`` fidelity is per-port idiom: in C++ (a synchronous httplib -/// client) an in-flight blocking socket read cannot be interrupted without a -/// thread, so cancellation is checked cooperatively *before* each attempt — the -/// honest, portable minimum. It is a non-owning pointer to a caller-owned +/// ``abort_signal`` is COOPERATIVE, not pre-emptive: this is a synchronous +/// httplib client, so an in-flight blocking socket read cannot be interrupted +/// without a thread. Cancellation is therefore checked *before* each attempt, +/// never during one. It is a non-owning pointer to a caller-owned /// ``std::atomic``; a truthy value raises the transport error before the /// send. ``nullptr`` == no cancellation (the default). struct RequestOptions { @@ -54,8 +53,7 @@ struct RequestOptions { /// Return ``*this`` with any set (non-empty) field of ``override_opts`` /// applied. This is the per-request-over-client-default shallow merge: an - /// unset field on ``override_opts`` leaves this value intact. Mirrors Python's - /// ``RequestOptions.merge``. + /// unset field on ``override_opts`` leaves this value intact. RequestOptions merge(const RequestOptions& override_opts) const { RequestOptions out = *this; if (override_opts.timeout.has_value()) { diff --git a/include/signalwire/security/security_utils.hpp b/include/signalwire/security/security_utils.hpp index cf7a38c..ea5a55b 100644 --- a/include/signalwire/security/security_utils.hpp +++ b/include/signalwire/security/security_utils.hpp @@ -5,18 +5,13 @@ #include #include -/// Standalone security-hygiene utilities. +/// Standalone security-hygiene utilities: keep credentials out of user +/// callbacks and logs, plus a reusable character-level hostname check. Three +/// pure free functions, no state, no I/O. /// -/// Mirrors the Python reference module -/// ``signalwire.core.security.security_utils`` (and the TypeScript SDK's -/// ``SecurityUtils``): keep credentials out of user callbacks and logs, plus a -/// reusable character-level hostname check. Three pure free functions, no -/// state, no I/O. -/// -/// Idiom note: the C++ port exposes these as PascalCase free functions in a -/// dedicated ``security_utils`` namespace (same convention used for -/// ``ValidateWebhookSignature`` in this module). The signature enumerator's -/// free-function rename table maps them back to the Python snake_case names. +/// These are PascalCase free functions in a dedicated ``security_utils`` +/// namespace — the same convention used for ``ValidateWebhookSignature`` in +/// this module. namespace signalwire { namespace security { namespace security_utils { diff --git a/include/signalwire/security/session_manager.hpp b/include/signalwire/security/session_manager.hpp index f6b33e3..99df24c 100644 --- a/include/signalwire/security/session_manager.hpp +++ b/include/signalwire/security/session_manager.hpp @@ -20,33 +20,30 @@ using json = nlohmann::json; /// is the base64url-encoding of the 5 dot-joined fields /// ``{call_id}.{function_name}.{expiry}.{nonce}.{signature}`` where /// ``signature = hex(hmac_sha256("{call_id}:{function_name}:{expiry}:{nonce}"))`` -/// and ``nonce`` is 16 hex chars (``secrets.token_hex(8)``). Validation +/// and ``nonce`` is 16 hex chars (8 random bytes, hex-encoded). Validation /// base64url-decodes, splits the 5 fields, recomputes the HMAC, and compares in /// CONSTANT time. class SessionManager { public: - /// Construct with the reference's constructor surface: - /// ``SessionManager(token_expiry_secs=900, secret_key=None)``. + /// Construct a session manager. /// /// ``token_expiry_secs`` is the lifetime applied to every token minted /// through ``generate_token`` / ``create_tool_token`` (and the default for /// ``create_token``) — this is what ``AgentBase(token_expiry_secs=...)`` /// forwards. ``secret_key`` is the HMAC signing key; when empty a fresh - /// 32-byte random key is generated and hex-encoded, mirroring the - /// reference's ``secrets.token_hex(32)``. + /// 32-byte random key is generated and hex-encoded. explicit SessionManager(int token_expiry_secs = 900, const std::string& secret_key = ""); - /// Construct with a raw byte secret (port convenience for tests that want + /// Construct with a raw byte secret (a convenience for tests that want /// deterministic key bytes). The bytes are hex-encoded into the same - /// ``secret_key`` string the reference-shaped constructor takes, so both - /// spellings sign identically. + /// ``secret_key`` string the other constructor takes, so both spellings + /// sign identically. explicit SessionManager(const std::vector& secret, int token_expiry_secs = 900); - // Construction parameters the reference keeps as public instance attributes. - /// reference: ``self.token_expiry_secs`` — the configured token lifetime. + /// The configured token lifetime. [[nodiscard]] int token_expiry_secs() const { return token_expiry_secs_; } - /// reference: ``self.secret_key`` — the HMAC signing key; the caller's value, - /// or the generated ``secrets.token_hex(32)``-shaped key when none was given. + /// The HMAC signing key; the caller's value, or the generated 32-byte + /// hex-encoded key when none was given. [[nodiscard]] const std::string& secret_key() const { return secret_key_; } /// Create a signed token for a function call @@ -80,17 +77,16 @@ class SessionManager { // C++ keeps the existing create_token/validate_token wire format untouched // and projects the reference names onto it (matching the Java/Ruby ports). - /// Mint a signed token — Python's ``generate_token``. Delegates to - /// ``create_token`` with the configured default expiry. + /// Mint a signed token. Delegates to ``create_token`` with the configured + /// default expiry. std::string generate_token(const std::string& function_name, const std::string& call_id) const; - /// Alias of ``generate_token`` — Python's ``create_tool_token``. + /// Alias of ``generate_token``. std::string create_tool_token(const std::string& function_name, const std::string& call_id) const; - /// Back-compat alias of ``validate_token`` — Python's - /// ``validate_tool_token(function_name, token, call_id)``. NOTE the - /// reference parameter order differs from ``validate_token``; this method - /// mirrors that order and delegates. + /// Back-compat alias of ``validate_token``. NOTE the parameter order differs + /// from ``validate_token`` — ``function_name`` comes FIRST here; this method + /// just reorders and delegates. [[nodiscard]] bool validate_tool_token(std::string_view function_name, std::string_view token, std::string_view call_id) const; @@ -102,8 +98,7 @@ class SessionManager { // success hook. State is guarded by a mutex so it is thread-safe. /// Return ``call_id`` when non-empty; otherwise mint a fresh URL-safe - /// session id (mirrors ``secrets.token_urlsafe(16)``). Creates the - /// session's metadata entry. + /// session id from 16 random bytes. Creates the session's metadata entry. std::string create_session(const std::string& call_id = ""); /// Legacy lifecycle hook — the manager is stateless w.r.t. activation, @@ -124,17 +119,15 @@ class SessionManager { bool set_session_metadata(const std::string& call_id, const std::string& key, const json& value); /// Enable/disable token-internals decoding in ``debug_token`` (off by - /// default). Mirrors the authoritative reference's ``_debug_mode`` gate. + /// default). void set_debug_mode(bool enabled); /// Decode a token's components for inspection WITHOUT validating it. /// Requires ``set_debug_mode(true)`` first; otherwise returns - /// ``{"error": "debug mode not enabled"}`` (matches the authoritative - /// reference). On a well-formed token returns + /// ``{"error": "debug mode not enabled"}``. On a well-formed token returns /// ``{valid_format, components, status}`` (call_id/signature truncated to /// 8 chars); on a malformed token returns ``{valid_format:false, ...}``. - /// Decodes this port's token format - /// (``base64(function:call_id:expiry).signature``). + /// Decodes the token format ``base64(function:call_id:expiry).signature``. [[nodiscard]] json debug_token(const std::string& token) const; private: @@ -147,36 +140,38 @@ class SessionManager { /// Base64 decode static std::string base64_decode(const std::string& encoded); - /// Base64url encode (URL-safe alphabet, no padding) — matches Python's - /// ``base64.urlsafe_b64encode(...).decode()`` used to wrap the whole token. + /// Base64url encode (URL-safe alphabet, PADDING INTACT) — used to wrap the + /// whole token, and it KEEPS the ``=`` padding. This used to strip the + /// padding; a strict base64url decoder REJECTS a stripped ``=``, so every + /// token minted here was undecodable by any strict consumer, even with a + /// correct key and a correct HMAC. Our own ``base64url_decode`` tolerates + /// missing padding, which is exactly why round-tripping against ourselves + /// never caught it. static std::string base64url_encode(const std::string& data); /// Base64url decode (URL-safe alphabet, tolerates missing padding) — - /// inverse of ``base64url_encode`` / Python's ``urlsafe_b64decode``. + /// inverse of ``base64url_encode``. static std::string base64url_decode(const std::string& encoded); /// Hex encode static std::string hex_encode(const std::vector& data); - /// Generate a random nonce of ``bytes`` bytes as a hex string — mirrors - /// Python's ``secrets.token_hex(bytes)`` (``2*bytes`` hex chars). + /// Generate a random nonce of ``bytes`` bytes as a hex string + /// (``2*bytes`` hex chars). static std::string token_hex(int bytes); /// Get current Unix timestamp static int64_t current_timestamp(); - /// HMAC signing key — the reference's ``self.secret_key``, a STRING whose - /// bytes are the HMAC key (``self.secret_key.encode()`` there). + /// HMAC signing key — a STRING whose bytes are used directly as the HMAC key. std::string secret_key_; /// Token lifetime in seconds, used by generate_token / create_tool_token /// (create_token still accepts an explicit override). Set from the - /// constructor's ``token_expiry_secs`` — the reference's - /// ``self.token_expiry_secs``. + /// constructor's ``token_expiry_secs``. int token_expiry_secs_ = 900; /// Per-session metadata store: call_id -> (key -> value). Guarded by - /// metadata_mutex_. A real store (not the reference's stateless no-op) so - /// the get/set metadata pair round-trips. + /// metadata_mutex_, so the get/set metadata pair round-trips safely. mutable std::mutex metadata_mutex_; std::map session_metadata_; diff --git a/include/signalwire/security/webhook_validator.hpp b/include/signalwire/security/webhook_validator.hpp index 91dcd47..a4d031e 100644 --- a/include/signalwire/security/webhook_validator.hpp +++ b/include/signalwire/security/webhook_validator.hpp @@ -34,10 +34,9 @@ namespace security { /// the raw body string. using FormParams = std::vector>>; -/// Drop-in shape for ``ValidateRequest`` mirroring -/// ``@signalwire/compatibility-api``'s ``RestClient.validateRequest``: -/// either a raw body string (delegates to the combined validator) or a -/// pre-parsed form-params list (runs Scheme B directly). +/// The request payload accepted by ``ValidateRequest``: either a raw body +/// string (delegates to the combined validator) or a pre-parsed form-params +/// list (runs Scheme B directly). using ParamsOrBody = std::variant; /// Validate a SignalWire webhook signature against both schemes. @@ -81,18 +80,15 @@ bool ValidateRequest(std::string_view signing_key, std::string_view signature, s const ParamsOrBody& params_or_raw_body); /// Response triple returned by ``Validate`` when a request must be -/// rejected: ``(status, headers, body)`` — the framework-free decision -/// core all ports share (Python ``webhook_middleware.validate``, dotnet -/// ``WebhookValidationMiddleware.Validate``, Rack/PSGI middleware). Status -/// is the HTTP status code, headers the response headers, body the -/// response body text. +/// rejected: ``(status, headers, body)``. Status is the HTTP status code, +/// headers the response headers, body the response body text. using ValidationResponse = std::tuple, std::string>; /// Framework-free webhook-validation decision core. This is the decomposed /// shape the SDK exposes so users can validate a signed inbound /// request WITHOUT depending on a specific HTTP framework — the /// cpp-httplib ``WrapWithSignatureValidation`` middleware is a thin -/// PORT_ADDITION idiom built on top of this. +/// convenience built on top of this. /// /// Pulls ``X-SignalWire-Signature`` (or the legacy ``X-Twilio-Signature`` /// alias) out of ``headers``, then runs ``ValidateWebhookSignature`` diff --git a/include/signalwire/server/agent_server.hpp b/include/signalwire/server/agent_server.hpp index a45534e..f5c5a2f 100644 --- a/include/signalwire/server/agent_server.hpp +++ b/include/signalwire/server/agent_server.hpp @@ -29,21 +29,19 @@ using json = nlohmann::json; /// Multi-agent hosting server class AgentServer { public: - /// @param log_level Logging level (debug, info, warning, error, critical), - /// stored lowercased exactly as the reference does and applied to the - /// process logger — the reference forwards it to uvicorn. + /// @param log_level Logging level (debug, info, warning, error, critical). + /// Stored lowercased and applied to the process logger. explicit AgentServer(const std::string& host = "0.0.0.0", int port = 3000, const std::string& log_level = "info"); ~AgentServer(); - // Construction parameters the reference keeps as public instance attributes - // (`self.host` / `self.port` / `self.log_level`) and reads back in `run()`. - /// reference: ``self.host`` — the bind host. + // Construction parameters, readable back; `run()` reads these to bind. + /// The bind host. [[nodiscard]] const std::string& host() const { return host_; } - /// reference: ``self.port`` — the bind port (a set ``PORT`` env var wins, - /// so this reports the port that will actually be bound). + /// The bind port. A set ``PORT`` env var wins, so this reports the port that + /// will actually be bound. [[nodiscard]] int port() const { return port_; } - /// reference: ``self.log_level`` — the lowercased level string. + /// The lowercased level string. [[nodiscard]] const std::string& log_level() const { return log_level_; } /// Register an agent at a specific route @@ -64,55 +62,47 @@ class AgentServer { /// Enable static file serving from a directory AgentServer& set_static_dir(const std::string& dir); - // ---- Public surface (signalwire.agent_server.AgentServer) -------- - // Python names for the operations above, so the cross-language surface lines - // up. These are the canonical spellings; the *_agent / *_sip_username / etc. - // forms are the C++-idiomatic aliases retained for existing callers. + // ---- Public surface -------- + // The canonical spellings for the operations above; the *_agent / + // *_sip_username / etc. forms are aliases retained for existing callers. - /// Register an agent at a route (Python: ``register``). When ``route`` is - /// empty the agent's own route is used, matching Python's ``route=None``. + /// Register an agent at a route. When ``route`` is empty the agent's own + /// route is used. AgentServer& register_(std::shared_ptr agent, const std::string& route = ""); - /// Unregister an agent by route; returns whether one was removed - /// (Python: ``unregister`` -> bool). + /// Unregister an agent by route; returns whether one was removed. bool unregister(const std::string& route); - /// All registered agents as (route, agent) pairs (Python: ``get_agents``). + /// All registered agents as (route, agent) pairs. std::vector>> get_agents() const; - /// Look up an agent by route; nullptr when absent (Python: ``get_agent``). - /// The route is normalized (leading ``/`` added) before lookup. + /// Look up an agent by route; nullptr when absent. The route is normalized + /// (leading ``/`` added) before lookup. std::shared_ptr get_agent(const std::string& route) const; - /// Enable SIP routing (Python: ``setup_sip_routing``). ``auto_map`` mirrors - /// Python's auto-mapping of each agent's SIP usernames. + /// Enable SIP routing at ``route``. ``auto_map`` requests auto-mapping of + /// each agent's own SIP usernames. AgentServer& setup_sip_routing(const std::string& route = "/sip", bool auto_map = true); - /// Map a SIP username to a route (Python: ``register_sip_username``). + /// Map a SIP username to a route. AgentServer& register_sip_username(const std::string& username, const std::string& route); /// Look up the route registered for a SIP username, case-insensitively; - /// returns an empty string when none is registered (Python: - /// ``AgentServer._lookup_sip_route`` — ``self._sip_username_mapping.get( - /// username.lower())``). + /// returns an empty string when none is registered. [[nodiscard]] std::string lookup_sip_route(const std::string& username) const; - /// The username -> route mapping (keys are lowercased). Python: - /// ``AgentServer._sip_username_mapping``. + /// The username -> route mapping (keys are lowercased). [[nodiscard]] std::map get_sip_username_mapping() const; - /// Serve static files from a directory at ``route`` (Python: - /// ``serve_static_files``). + /// Serve static files from a directory at ``route``. AgentServer& serve_static_files(const std::string& directory, const std::string& route = "/"); /// A routing callback: given a request path + query params, return the route - /// to dispatch to (empty = no override). Mirrors Python's - /// ``Callable[[Request, dict], str | None]`` contract. + /// to dispatch to (empty = no override). using GlobalRoutingCallback = std::function; - /// Register a routing callback across all agents at ``path`` (Python: - /// ``register_global_routing_callback``). + /// Register a routing callback across all agents at ``path``. AgentServer& register_global_routing_callback(GlobalRoutingCallback callback_fn, const std::string& path); diff --git a/include/signalwire/signalwire.hpp b/include/signalwire/signalwire.hpp index dd1b47b..d19aab3 100644 --- a/include/signalwire/signalwire.hpp +++ b/include/signalwire/signalwire.hpp @@ -26,24 +26,18 @@ namespace signalwire { -/// Top-level convenience entry points — mirror Python's -/// ``signalwire/__init__.py`` package-level helpers (``RestClient``, -/// ``register_skill``, ``add_skill_directory``, -/// ``list_skills_with_params``). -/// -/// The audit projects each free function onto the canonical Python -/// ``signalwire.`` path. ``RestClient`` preserves PascalCase to -/// match Python's same-cased factory function name. +/// Top-level convenience entry points in namespace ``signalwire`` — +/// ``RestClient``, ``register_skill``, ``add_skill_directory``, +/// ``list_skills``, and ``list_skills_with_params``. ``RestClient`` is +/// deliberately PascalCase: it is a factory function, not a type. /// Construct a ``rest::RestClient`` from positional or keyword /// credentials. /// -/// Mirrors Python's top-level ``signalwire.RestClient(*args, **kwargs)`` -/// factory — a thin wrapper that lazy-imports -/// ``signalwire.rest.RestClient`` and instantiates it. Supports both -/// positional credentials (``args = {project, token, space}``) and -/// keyword credentials (``kwargs["project"]`` etc.) with -/// environment-variable fallback. +/// A thin factory over ``rest::RestClient``. Supports both positional +/// credentials (``args = {project, token, space}``) and keyword +/// credentials (``kwargs["project"]`` etc.) with environment-variable +/// fallback. /// /// @throws std::invalid_argument when credentials cannot be derived /// from either ``args``, ``kwargs``, or the standard @@ -54,7 +48,6 @@ namespace signalwire { /// Register a custom skill class with the global skill registry. /// -/// Mirrors Python's ``signalwire.register_skill(skill_class)``. /// Delegates to ``skills::SkillRegistry::register_skill``. The skill's /// name comes from the supplied ``skills::SkillBase`` factory (which /// instantiates a SkillBase to read its ``skill_name()`` accessor). @@ -62,8 +55,7 @@ void register_skill(skills::SkillFactory factory); /// Add a directory to search for skills. /// -/// Mirrors Python's ``signalwire.add_skill_directory(path)`` — -/// delegates to the singleton ``skills::SkillRegistry`` instance so +/// Delegates to the singleton ``skills::SkillRegistry`` instance so /// third-party skill collections can be registered by path. /// /// @throws std::invalid_argument when the path doesn't exist or @@ -72,22 +64,20 @@ void add_skill_directory(const std::string& path); /// Get complete schema for all available skills. /// -/// Mirrors Python's ``signalwire.list_skills_with_params()``. Returns -/// a map keyed by skill name where each value contains parameter +/// Returns a map keyed by skill name where each value contains parameter /// metadata. Useful for GUI configuration tools, API documentation, /// or programmatic skill discovery. /// -/// C++ skills don't carry rich Python-style parameter introspection -/// in v1, so each entry contains the skill name and an empty parameter -/// map; built-in skills that expose ``parameter_schema()`` via -/// ``SkillBase`` get richer detail merged in. +/// There is no runtime parameter introspection in v1, so an entry is the +/// skill name plus an empty parameter map by default; built-in skills that +/// expose ``parameter_schema()`` via ``SkillBase`` get richer detail merged +/// in. [[nodiscard]] std::map> list_skills_with_params(); /// List all available skills with lightweight metadata. /// -/// Mirrors Python's top-level ``signalwire.list_skills()`` — one record per -/// registered skill (name plus description/version where the factory can be -/// instantiated). The lighter summary counterpart to +/// One record per registered skill (name plus description/version where the +/// factory can be instantiated). The lighter summary counterpart to /// ``list_skills_with_params()``; both delegate to the singleton /// ``skills::SkillRegistry``. [[nodiscard]] std::vector> list_skills(); diff --git a/include/signalwire/skills/claude_skills_core.hpp b/include/signalwire/skills/claude_skills_core.hpp index 7bec809..77c7800 100644 --- a/include/signalwire/skills/claude_skills_core.hpp +++ b/include/signalwire/skills/claude_skills_core.hpp @@ -4,13 +4,13 @@ // Shared core for the claude_skills skill's SKILL.md discovery + tool building. // -// Ports Python signalwire/skills/claude_skills/skill.py: each immediate -// subdirectory of skills_path that contains a SKILL.md is discovered, its YAML -// frontmatter (name/description) parsed, and one SWAIG tool declared per skill -// (name = {tool_prefix}{sanitized-name}, description from the frontmatter, -// handler returns the SKILL.md body). NATIVE EXECUTION of skill scripts is -// impossible in this AOT port, so the port discovers + declares the tools and -// serves their instructions; it does not run embedded code. +// Each immediate subdirectory of skills_path that contains a SKILL.md is +// discovered, its YAML frontmatter (name/description) parsed, and one SWAIG +// tool declared per skill (name = {tool_prefix}{sanitized-name}, description +// from the frontmatter, handler returns the SKILL.md body). Executing a +// skill's scripts is not possible from an ahead-of-time-compiled binary, so +// this discovers + declares the tools and serves their instructions; it does +// not run embedded code. // // This header is included by BOTH claude_skills implementations in the tree — // the registered `ClaudeSkillsSkillR` in skill_registry.cpp and the @@ -127,7 +127,7 @@ inline bool parse_skill_md(const fs::path& path, DiscoveredSkill& out) { } /// Sanitize a skill name into a SWAIG-safe tool suffix (lowercase, non -/// [a-z0-9_] -> '_'), mirroring the reference's ``_sanitize_tool_name``. +/// [a-z0-9_] -> '_'). inline std::string sanitize_tool_name(const std::string& name) { std::string out; out.reserve(name.size()); diff --git a/include/signalwire/skills/skill_base.hpp b/include/signalwire/skills/skill_base.hpp index b85514d..ff1136f 100644 --- a/include/signalwire/skills/skill_base.hpp +++ b/include/signalwire/skills/skill_base.hpp @@ -32,12 +32,10 @@ class SkillManager; /// Abstract base class for all skills class SkillBase { - // The reference constructs a skill as `SkillClass(agent, params)`, so the - // agent + params are set BY THE LOADER, not by user code. C++ default- - // constructs the instance through the registry factory, so the loader injects - // them afterwards via `bind` — which is therefore private to SkillManager, - // keeping the construction contract identical (a skill cannot re-parent - // itself) rather than adding a public setter the reference lacks. + // A skill's agent + params are set BY THE LOADER, not by user code. The + // instance is default-constructed through the registry factory, so the loader + // injects them afterwards via `bind` — which is therefore private to + // SkillManager, so a skill cannot re-parent itself. friend class SkillManager; public: @@ -86,11 +84,10 @@ class SkillBase { virtual void cleanup() {} // ======================================================================== - // Public surface (signalwire.core.skill_base.SkillBase) + // Public surface // ======================================================================== /// Check that every required env var (required_env_vars()) is set. - /// Corresponds to ``SkillBase.validate_env_vars``. [[nodiscard]] bool validate_env_vars() const { for (const auto& var : required_env_vars()) { const char* v = std::getenv(var.c_str()); @@ -103,11 +100,11 @@ class SkillBase { /// Check that every required package is available. C++ links its deps at /// build time (there is no runtime import), so a compiled skill's packages - /// are inherently present — return true. Corresponds to ``validate_packages``. + /// are inherently present — always returns true. [[nodiscard]] bool validate_packages() const { return true; } /// Read this skill instance's namespaced state from a SWAIG handler's raw - /// global_data. Corresponds to ``get_skill_data``. + /// global_data. [[nodiscard]] json get_skill_data(const json& raw_data) const { const std::string ns = skill_namespace(); json global_data = raw_data.value("global_data", json::object()); @@ -115,24 +112,21 @@ class SkillBase { } /// Write this skill instance's namespaced state into a FunctionResult (under - /// the skill's namespace key). Corresponds to ``update_skill_data``. + /// the skill's namespace key). swaig::FunctionResult& update_skill_data(swaig::FunctionResult& result, const json& data) const { result.update_global_data(json::object({{skill_namespace(), data}})); return result; } - // Construction state the reference keeps as public instance attributes - // (`SkillBase.__init__(agent, params)` sets `self.agent` / `self.params`). - // The port binds them at load time via `bind` rather than through the ctor, - // because a C++ skill is default-constructed by the registry factory and - // then handed its agent + params — the values and their lifetime are the - // same, only the injection point differs. + // Construction state, bound at load time via `bind` rather than through the + // constructor: a skill is default-constructed by the registry factory and + // then handed its agent + params. - /// The agent this skill was loaded into (reference: ``self.agent``). - /// ``nullptr`` before ``bind``; ``SkillManager::load_skill`` always binds. + /// The agent this skill was loaded into. ``nullptr`` before ``bind``; + /// ``SkillManager::load_skill`` always binds. [[nodiscard]] agent::AgentBase* agent() const { return agent_; } - /// The parameters this skill was loaded with (reference: ``self.params``). + /// The parameters this skill was loaded with. [[nodiscard]] const json& params() const { return params_; } // ======================================================================== @@ -140,8 +134,7 @@ class SkillBase { // ======================================================================== /// Define a tool (convenience for register_tools implementations). - /// ``secure`` defaults to TRUE — the reference's ``SkillBase.define_tool`` - /// delegates to ``agent.define_tool``, whose default is ``secure=True``. + /// ``secure`` defaults to TRUE, matching ``AgentBase::define_tool``. [[nodiscard]] swaig::ToolDefinition define_tool(const std::string& name, const std::string& description, const json& parameters, @@ -181,8 +174,8 @@ class SkillBase { protected: /// The global_data namespace for this skill instance: ``skill:`` when - /// a ``prefix`` param is set, else ``skill:``. Protected — mirrors - /// Python's private ``_get_skill_namespace`` (off the public surface). + /// a ``prefix`` param is set, else ``skill:``. Protected — it is + /// an implementation detail, not part of the public surface. [[nodiscard]] std::string skill_namespace() const { if (params_.contains("prefix") && params_["prefix"].is_string()) { return "skill:" + params_["prefix"].get(); @@ -199,8 +192,7 @@ class SkillBase { /// Bind the loading agent + params onto this instance. Called by /// ``SkillManager::load_skill`` before ``setup``, so a skill's own /// ``setup``/``register_tools`` can read ``agent()`` and ``params()``. - /// Private + friended to SkillManager: the reference sets both through the - /// constructor, so only the loader may do it here too. + /// Private + friended to SkillManager: only the loader may set them. void bind(agent::AgentBase* owner, const json& params) { agent_ = owner; params_ = params.is_object() ? params : json::object(); diff --git a/include/signalwire/skills/skill_manager.hpp b/include/signalwire/skills/skill_manager.hpp index b2f9c23..f4b43cc 100644 --- a/include/signalwire/skills/skill_manager.hpp +++ b/include/signalwire/skills/skill_manager.hpp @@ -26,18 +26,28 @@ class SkillManager { public: SkillManager() = default; - /// Construct bound to the agent this manager loads skills into. Mirrors the - /// reference's ``SkillManager(agent)``. + /// Construct bound to the agent this manager loads skills into. explicit SkillManager(agent::AgentBase& agent) : agent_(&agent) {} - /// The agent this manager loads skills into (reference: ``self.agent``). - /// ``nullptr`` for a default-constructed manager, which takes the agent - /// per ``load_skill`` call instead. + /// The agent this manager loads skills into. ``nullptr`` for a + /// default-constructed manager, on which ``load_skill`` fails loud — a + /// manager is normally agent-bound. [[nodiscard]] agent::AgentBase* agent() const { return agent_; } - /// Load a skill by name with params and register it with the agent - [[nodiscard]] bool load_skill(const std::string& skill_name, const json& params, - agent::AgentBase& agent); + /// Load a skill by name and register it with this manager's bound agent. + /// + /// BOTH trailing parameters are optional. + /// + /// @param skill_class Optional explicit factory for the skill. When absent, + /// the skill is looked up in ``SkillRegistry`` by name via + /// ``SkillRegistry::get_skill_class(skill_name)``. + /// @param params Optional parameters handed to the skill's setup. + /// + /// Requires a manager constructed with an agent; returns false and logs when + /// there is none (use the explicit-agent overload in that case). + [[nodiscard]] bool load_skill(const std::string& skill_name, + const std::optional& skill_class = std::nullopt, + const std::optional& params = std::nullopt); /// Unload a skill void unload_skill(const std::string& skill_name); @@ -48,24 +58,31 @@ class SkillManager { /// List loaded skills [[nodiscard]] std::vector list_loaded() const; - // ---- Public surface (signalwire.core.skill_manager.SkillManager) -- + // ---- Public surface ------------------------------------------------ - /// Whether a skill is loaded (Python: ``has_skill``). Alias of is_loaded. + /// Whether a skill is loaded. Alias of ``is_loaded``. [[nodiscard]] bool has_skill(const std::string& skill_name) const { return is_loaded(skill_name); } - /// List loaded skill names (Python: ``list_loaded_skills``). Alias of list_loaded. + /// List loaded skill names. Alias of ``list_loaded``. [[nodiscard]] std::vector list_loaded_skills() const { return list_loaded(); } - /// Get a loaded skill instance by name, or nullptr if not loaded - /// (Python: ``get_skill``). + /// Get a loaded skill instance by name, or nullptr if not loaded. [[nodiscard]] SkillBase* get_skill(const std::string& skill_name) const; /// Cleanup all skills void cleanup_all(); private: + /// One skill the manager has loaded, and everything needed to unload it. + /// + /// `name` is the registry key `load_skill`/`unload_skill`/`is_loaded` match + /// on. `instance` OWNS the constructed skill — the manager's `unique_ptr` is + /// what keeps it alive, so `get_skill` hands back a non-owning `SkillBase*` + /// that is valid only until that skill is unloaded or `cleanup_all` runs. + /// `params` retains the configuration the skill was loaded with. Entries are + /// kept in a vector, so `list_loaded` reports load order. struct LoadedSkill { std::string name; std::unique_ptr instance; diff --git a/include/signalwire/skills/skill_name.hpp b/include/signalwire/skills/skill_name.hpp index adef555..2a4aad1 100644 --- a/include/signalwire/skills/skill_name.hpp +++ b/include/signalwire/skills/skill_name.hpp @@ -12,17 +12,17 @@ namespace skills { /// `AgentBase::add_skill()` (and `remove_skill()` / `has_skill()`) accept this /// `enum class` OR a `std::string`. The enum gives editor autocompletion and /// makes a typo fail at the call site — a bare string like `"datetiem"` only -/// fails at runtime, on the server. The string overload matches the -/// Python reference (which uses a bare `str`) and still allows custom / -/// third-party skills that aren't built in. +/// fails at runtime, on the server. The string overload keeps the set OPEN, so +/// custom / third-party skills that aren't built in still work. /// /// agent.add_skill(SkillName::Datetime); // typed, autocompleted /// agent.add_skill("datetime"); // string still works /// agent.add_skill("my_custom_skill"); // open set: custom skills ok /// -/// Members mirror the 18 built-in skills' registered `skill_name()` values -/// (the canonical wire strings). `skill_name_value()` maps each member to that -/// wire string, so the enum and string overloads load the identical skill. +/// Members correspond one-to-one with the 18 built-in skills' registered +/// `skill_name()` values (the canonical wire strings). `skill_name_value()` +/// maps each member to that wire string, so the enum and string overloads load +/// the identical skill. enum class SkillName { ApiNinjasTrivia, ClaudeSkills, diff --git a/include/signalwire/skills/skill_registry.hpp b/include/signalwire/skills/skill_registry.hpp index 0ec9431..0161a34 100644 --- a/include/signalwire/skills/skill_registry.hpp +++ b/include/signalwire/skills/skill_registry.hpp @@ -28,9 +28,25 @@ class SkillRegistry { return registry; } - /// Register a skill factory + /// Register a skill factory. + /// + /// A second registration of a name that is already registered is ALWAYS a + /// bug, and it throws. Silently overwriting would let two classes claim the + /// same skill name: static-initialization order ACROSS translation units is + /// unspecified in C++, so which implementation a caller actually got would + /// depend on link order — and could change between builds without any source + /// change. Failing loud makes that state unrepresentable. + /// + /// Re-registering the SAME factory is not detectable (``std::function`` has + /// no equality), so idempotent "register if absent" callers must ask + /// ``has_skill`` first — see ``ensure_builtin_skills_registered``. void register_skill(const std::string& name, SkillFactory factory) { std::lock_guard lock(mutex_); + if (factories_.find(name) != factories_.end()) { + throw std::invalid_argument("Duplicate skill registration for name: " + name + + " (a skill name may be registered exactly once; the second " + "registration would silently replace the first)"); + } factories_[name] = std::move(factory); } @@ -63,12 +79,10 @@ class SkillRegistry { /// Add a directory to search for skills. /// - /// Mirrors Python's - /// ``signalwire.skills.registry.SkillRegistry.add_skill_directory``: - /// validate that the path exists and is a directory, then append it - /// (de-duplicated) to ``external_paths_``. Throws - /// ``std::invalid_argument`` (the C++ analog of Python's ``ValueError``) - /// for invalid input — the path doesn't exist or isn't a directory. + /// Validates that the path exists and is a directory, then appends it + /// (de-duplicated) to the external search paths. Throws + /// ``std::invalid_argument`` when the path doesn't exist or isn't a + /// directory. void add_skill_directory(const std::string& path) { std::lock_guard lock(mutex_); struct stat st; @@ -86,16 +100,14 @@ class SkillRegistry { external_paths_.push_back(path); } - /// Look up a skill factory by name (Python: - /// ``SkillRegistry.get_skill_class`` — returns the skill *type*). C++ has no - /// first-class ``type`` object, so this returns whether the skill is known - /// (the factory exists); use ``create`` to instantiate. Mirrors the - /// discovery-by-name contract. + /// Look up a skill factory by name. C++ has no first-class ``type`` object, + /// so this returns whether the skill is known (its factory exists); use + /// ``create`` to instantiate one. [[nodiscard]] bool get_skill_class(const std::string& name) const { return has_skill(name); } - /// Discover all registered skills as ``{name, ...}`` records (Python: - /// ``SkillRegistry.discover_skills`` -> list of dicts). Each record carries - /// the skill name and its instance-level schema where available. + /// Discover all registered skills as an ARRAY of ``{name, ...}`` records. + /// Each record carries the skill name and its instance-level schema where + /// available. [[nodiscard]] nlohmann::json discover_skills() const { std::lock_guard lock(mutex_); nlohmann::json out = nlohmann::json::array(); @@ -107,8 +119,8 @@ class SkillRegistry { return out; } - /// Return every registered skill's parameter schema keyed by skill name - /// (Python: ``SkillRegistry.get_all_skills_schema`` -> dict[name -> schema]). + /// Return every registered skill's parameter schema as a JSON OBJECT keyed by + /// skill name (name -> schema). [[nodiscard]] nlohmann::json get_all_skills_schema() const { std::lock_guard lock(mutex_); nlohmann::json out = nlohmann::json::object(); @@ -120,9 +132,9 @@ class SkillRegistry { } /// Return the source (built-in vs external directory) each skill was loaded - /// from (Python: ``SkillRegistry.list_all_skill_sources`` -> dict[source -> - /// list of names]). Built-in factories are grouped under ``"builtin"``; the - /// registered external directories are listed under ``"external"``. + /// from, as a JSON OBJECT of source -> list of names. Built-in factories are + /// grouped under ``"builtin"``; the registered external directories are + /// listed under ``"external"``. [[nodiscard]] nlohmann::json list_all_skill_sources() const { std::lock_guard lock(mutex_); nlohmann::json out = nlohmann::json::object(); @@ -152,10 +164,9 @@ class SkillRegistry { /// Returns the effective external skill directories: the ones registered via /// ``add_skill_directory`` PLUS any supplied through the /// ``SIGNALWIRE_SKILL_PATHS`` environment variable (colon-separated, deduped, - /// registered paths first). Mirrors Python's ``SkillRegistry`` search order, - /// which appends ``os.environ["SIGNALWIRE_SKILL_PATHS"]`` (split on - /// ``os.pathsep``) to the registered ``_external_paths`` when resolving a - /// skill by name. + /// registered paths first). This is the search order used when resolving a + /// skill by name: registered directories are consulted before the + /// environment-supplied ones. [[nodiscard]] std::vector external_paths() const { std::lock_guard lock(mutex_); std::vector paths = external_paths_; @@ -177,7 +188,7 @@ class SkillRegistry { private: /// Parse the ``SIGNALWIRE_SKILL_PATHS`` env var into a list of directories /// (colon-separated, empty entries dropped). Read on every call so a var set - /// after construction still takes effect, matching Python's search-time read. + /// after construction still takes effect. [[nodiscard]] static std::vector env_skill_paths_locked() { std::vector out; const char* raw = std::getenv("SIGNALWIRE_SKILL_PATHS"); diff --git a/include/signalwire/skills/web_search_core.hpp b/include/signalwire/skills/web_search_core.hpp index 5ff52b6..e299337 100644 --- a/include/signalwire/skills/web_search_core.hpp +++ b/include/signalwire/skills/web_search_core.hpp @@ -4,7 +4,7 @@ // Shared core for the web_search skill's latency-control + result formatting. // -// Ports Python signalwire/skills/web_search/skill.py (51101da + 295745b): +// The latency-control knobs: // per_page_timeout — per-page fetch timeout (the HTTP client's per-request // timeout). Default 2.0s. // overall_deadline — wall-clock budget for the whole tool call, enforced @@ -86,7 +86,7 @@ struct LatencyParams { /// Format Google CSE snippets without fetching the underlying pages. Used for /// the `snippets_only` fast path AND as the graceful fallback when scraping is /// abandoned by the overall_deadline. Always non-empty when there is at least -/// one candidate. Mirrors Python's `_format_snippet_results`. +/// one candidate. [[nodiscard]] inline std::string format_snippet_results(const std::string& query, const std::vector& cands, int num_results) { @@ -281,7 +281,7 @@ struct LatencyParams { /// returned zero items" (return empty_no_items_message) from "CSE returned /// items but none scraped" (snippet fallback). /// -/// Steps mirror Python's search_and_scrape_best: +/// Steps, in order: /// 1. snippets_only -> format CSE snippets directly. /// 2. scrape under deadline. /// 3. no scraped survivors -> snippet fallback (non-empty). @@ -334,8 +334,7 @@ struct LatencyParams { } /// Build the parameter-schema fragment advertising the 6 latency / response -/// params. Merged into each skill's get_parameter_schema(). Mirrors Python's -/// get_parameter_schema entries (295745b) and the Go reference port. +/// params. Merged into each skill's get_parameter_schema(). [[nodiscard]] inline json schema_fragment() { return json::object( {{"response_prefix", diff --git a/include/signalwire/swaig/function_result.hpp b/include/signalwire/swaig/function_result.hpp index 44a5f51..b6b8435 100644 --- a/include/signalwire/swaig/function_result.hpp +++ b/include/signalwire/swaig/function_result.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace signalwire { @@ -96,22 +97,22 @@ enum class CallbackMethod { Get, Post }; // =========================================================================== /// Recording container format for `FunctionResult::record_call`. -/// Mirrors the reference's `format in {"wav","mp3","mp4"}` validation. +/// The validated set is `format in {"wav","mp3","mp4"}`. enum class RecordFormat { Wav, Mp3, Mp4 }; /// Audio direction for `FunctionResult::record_call`. -/// Mirrors the reference's `direction in {"speak","listen","both"}` validation. +/// The validated set is `direction in {"speak","listen","both"}`. /// NOTE: differs from `TapDirection` — record_call uses `listen`, tap uses `hear`. enum class RecordDirection { Speak, Listen, Both }; /// Audio direction for `FunctionResult::tap`. -/// Mirrors the reference's `direction in {"speak","hear","both"}` validation. +/// The validated set is `direction in {"speak","hear","both"}`. /// NOTE: differs from `RecordDirection` — tap uses `hear`, record_call uses `listen`. enum class TapDirection { Speak, Hear, Both }; /// Media codec for `FunctionResult::tap` (SWAIG tap only). -/// Mirrors the reference's `codec in {"PCMU","PCMA"}` validation. The wire -/// strings are upper-case. Distinct from the wider RELAY codec set — do not unify. +/// The validated set is `codec in {"PCMU","PCMA"}`. The wire strings are +/// upper-case. Distinct from the wider RELAY codec set — do not unify. enum class Codec { Pcmu, Pcma }; [[nodiscard]] inline std::string record_format_value(RecordFormat v) { @@ -168,16 +169,26 @@ enum class Codec { Pcmu, Pcma }; /// /// `JoinConferenceOptions::beep = ConferenceBeep::OnEnter;` and /// `... = "onEnter";` both compile and resolve to the same wire string; the -/// open-string path matches Python's bare `str` (the validation in -/// `join_conference` then rejects out-of-set strings exactly as Python does). +/// open-string path keeps out-of-set values expressible, and the validation in +/// `join_conference` then rejects out-of-set strings. /// Templated on the enum type plus its `*_value()` mapper so one definition /// covers all four sets. template +/// @tparam E the closed-set enum this field accepts. +/// @tparam Map the enum's `*_value()` mapper — the single point where an +/// enumerator becomes its wire string, so the typed and the string path can +/// never disagree. struct EnumOrString { + /// The resolved wire string. Normalized at construction: an enum operand is + /// mapped through `Map`, a string operand is stored verbatim. std::string value; - EnumOrString(E e) : value(Map(e)) {} // NOLINT(google-explicit-constructor) + /// Implicit from the enum — mapped to its wire string. + EnumOrString(E e) : value(Map(e)) {} // NOLINT(google-explicit-constructor) + /// Implicit from a string — stored as-is, unvalidated. EnumOrString(const std::string& s) : value(s) {} // NOLINT - EnumOrString(const char* s) : value(s) {} // NOLINT + /// Implicit from a string literal, so `= "onEnter"` needs no cast. + EnumOrString(const char* s) : value(s) {} // NOLINT + /// The wire string this field serializes to. [[nodiscard]] const std::string& str() const { return value; } }; @@ -188,11 +199,10 @@ using MethodField = EnumOrString; /// Options bag for `FunctionResult::join_conference`. /// -/// Every field is `std::optional` and unset means "Python default" — so a +/// Every field is `std::optional` and unset means "omit from the wire" — so a /// default-constructed `JoinConferenceOptions` collapses to the bare -/// conference-name string form, matching the reference's simple case. Closed -/// sets use the enum-or-string wrapper above; open fields are plain -/// `std::optional`. `result` is a free-form `json` (Python's `Optional[Any]`). +/// conference-name string form. Closed sets use the enum-or-string wrapper +/// above; open fields are plain `std::optional`. `result` is a free-form `json`. struct JoinConferenceOptions { std::optional muted; std::optional beep; @@ -223,12 +233,11 @@ class FunctionResult { // Core // ======================================================================== - /// The spoken/returned text (reference: ``self.response``) — emitted as the - /// ``response`` key when non-empty. Readable back after construction or a - /// ``set_response`` call. + /// The spoken/returned text — emitted as the ``response`` key when non-empty. + /// Readable back after construction or a ``set_response`` call. [[nodiscard]] const std::string& response() const { return response_; } - /// Whether the AI processes the result before speaking (reference: - /// ``self.post_process``) — emitted only alongside an action. + /// Whether the AI processes the result before speaking — emitted only + /// alongside an action. [[nodiscard]] bool post_process() const { return post_process_; } FunctionResult& set_response(const std::string& response); @@ -265,7 +274,11 @@ class FunctionResult { FunctionResult& switch_context(const std::string& system_prompt = "", const std::string& user_prompt = "", bool consolidate = false, bool full_reset = false); - FunctionResult& replace_in_history(const json& text); + /// After first send, replace the tool_call+result pair in conversation + /// history. ``text`` is a STRING (replace the tool_call with an assistant + /// message carrying this text) or ``true`` (remove the pair entirely). + /// Defaults to ``true``. + FunctionResult& replace_in_history(const json& text = true); // ======================================================================== // Media @@ -316,14 +329,13 @@ class FunctionResult { FunctionResult& execute_swml(const json& swml_content, bool transfer = false); - /// Join an ad-hoc audio conference (SWML `join_conference`). Full support - /// with Python `core/function_result.py`: 18 optional params past `name`, - /// 7 validations, and simple (bare-name) vs full-object emission. + /// Join an ad-hoc audio conference (SWML `join_conference`). 18 optional + /// params past `name`, 7 validations, and simple (bare-name) vs full-object + /// emission. /// - /// Flat positional overload — mirrors the Python signature 1:1 so the - /// cross-language audit lines up on parameter count/types. The closed-set - /// params are bare `std::string` (Python uses bare `str`); the - /// options-struct overload below adds the typed `enum class` affordance. + /// Flat positional overload. The closed-set params are bare `std::string` + /// here; the options-struct overload below adds the typed `enum class` + /// affordance. FunctionResult& join_conference( const std::string& name, bool muted = false, const std::string& beep = "true", bool start_on_enter = true, bool end_on_exit = false, @@ -366,8 +378,8 @@ class FunctionResult { const std::string& input_method = "dtmf", const std::string& status_url = "", const std::string& payment_method = "credit-card", int timeout = 5, int max_attempts = 1, bool security_code = true, - const std::string& postal_code = "true", int min_postal_code_length = 0, - const std::string& token_type = "reusable", + const std::variant& postal_code = true, + int min_postal_code_length = 0, const std::string& token_type = "reusable", const std::string& charge_amount = "", const std::string& currency = "usd", const std::string& language = "en-US", const std::string& voice = "woman", const std::string& description = "", diff --git a/include/signalwire/swaig/tool_definition.hpp b/include/signalwire/swaig/tool_definition.hpp index bff5458..64e0b60 100644 --- a/include/signalwire/swaig/tool_definition.hpp +++ b/include/signalwire/swaig/tool_definition.hpp @@ -18,9 +18,8 @@ struct ToolDefinition { std::string description; json parameters; // JSON schema for parameters ToolHandler handler; - /// Whether this tool requires SWAIG token validation. Defaults to TRUE - /// fleet-wide (reference: ``tool_mixin.define_tool(secure=True)``) — a tool - /// defined without an explicit ``secure`` is SECURE, so its rendered webhook + /// Whether this tool requires SWAIG token validation. Defaults to TRUE — a + /// tool defined without an explicit ``secure`` is SECURE, so its rendered webhook /// carries the per-tool ``__token`` and its dispatch validates it. Defaulting /// this to false would silently ship every tool unauthenticated. bool secure = true; diff --git a/include/signalwire/swaig/type_inference.hpp b/include/signalwire/swaig/type_inference.hpp index faa6a47..6f396a6 100644 --- a/include/signalwire/swaig/type_inference.hpp +++ b/include/signalwire/swaig/type_inference.hpp @@ -36,8 +36,8 @@ using json = nlohmann::json; // functions onto the Python module-level functions in the enumerators. // =========================================================================== -/// The inferred-schema 5-tuple (Python's -/// ``(parameters, required, description, is_typed, has_raw_data)``): +/// The inferred-schema 5-tuple +/// ``(parameters, required, description, is_typed, has_raw_data)``: /// - parameters: the JSON-Schema ``properties`` object (name -> property). /// - required: the list of required parameter names. /// - description: the tool description (nullopt when none supplied). @@ -46,23 +46,20 @@ using json = nlohmann::json; using InferredSchema = std::tuple, std::optional, bool, bool>; -/// Infer a JSON-Schema for a SWAIG tool's parameters from the port's typed -/// ``ParameterSchema`` params-builder (Python: -/// ``infer_schema(func) -> (parameters, required, description, is_typed, -/// has_raw_data)``). The ``raw_data`` property is the SWAIG raw-payload channel -/// and is excluded from the emitted ``parameters``/``required`` — its presence -/// is reported via ``has_raw_data`` instead, matching the reference. +/// Infer a JSON-Schema for a SWAIG tool's parameters from the typed +/// ``ParameterSchema`` params-builder. The ``raw_data`` property is the SWAIG +/// raw-payload channel and is excluded from the emitted +/// ``parameters``/``required`` — its presence is reported via ``has_raw_data`` +/// instead. /// /// @param params the typed schema built via ``ParameterSchema``. -/// @param description optional tool description (Python derives this from the -/// handler's docstring; C++ lambdas carry none, so the -/// caller supplies it — default nullopt). +/// @param description optional tool description. C++ lambdas carry no +/// docstring, so the caller supplies it — default nullopt. [[nodiscard]] InferredSchema infer_schema( const ParameterSchema& params, const std::optional& description = std::nullopt); /// Wrap a typed handler so it can be invoked with the standard SWAIG calling -/// convention ``(args, raw_data)`` (Python: -/// ``create_typed_handler_wrapper(func, has_raw_data) -> wrapper``). The +/// convention ``(args, raw_data)``. The /// wrapper normalizes the ``args`` object to a JSON object and forwards it; /// when ``has_raw_data`` is set it also forwards the raw payload, otherwise it /// passes an empty object so the wrapped handler never sees the raw channel it diff --git a/include/signalwire/swml/service.hpp b/include/signalwire/swml/service.hpp index 0b296db..db0288d 100644 --- a/include/signalwire/swml/service.hpp +++ b/include/signalwire/swml/service.hpp @@ -40,20 +40,16 @@ class Service { public: /// Construct a SWML service. /// - /// Mirrors the reference ``SWMLService.__init__(name, route, host, port, - /// basic_auth, schema_path, config_file, schema_validation)`` — every - /// parameter is forwarded to the same collaborator the reference forwards - /// it to: + /// Each parameter is forwarded to its collaborator: /// * ``schema_path`` + ``schema_validation`` → ``SchemaUtils`` - /// (``self.schema_utils``), - /// * ``config_file`` → ``SecurityConfig`` (``self.security``) and the - /// ``service`` section that supplies name/route/host/port defaults, - /// * ``basic_auth`` → the auth credentials (``set_auth``). + /// (see ``schema_utils()``), + /// * ``config_file`` → ``SecurityConfig`` and the ``service`` section that + /// supplies name/route/host/port defaults, + /// * ``basic_auth`` → the auth credentials (see ``set_auth``). /// - /// ``port``/``basic_auth``/``schema_path``/``config_file`` are optional in - /// the reference; an empty ``std::optional`` here means "not supplied", - /// which is what lets the PORT env var (port) and the config-file / - /// environment fallbacks (auth) still apply. + /// For ``port``/``basic_auth``/``schema_path``/``config_file``, an empty + /// ``std::optional`` means "not supplied", which is what lets the PORT env + /// var (port) and the config-file / environment fallbacks (auth) still apply. explicit Service( const std::string& name = "service", const std::string& route = "/", const std::string& host = "0.0.0.0", const std::optional& port = std::nullopt, @@ -76,8 +72,7 @@ class Service { /// Set the host to bind to Service& set_host(const std::string& host); - /// The host this service binds to (reference: ``self.host``) — the twin of - /// the existing ``port()`` accessor. + /// The host this service binds to — the twin of the ``port()`` accessor. const std::string& host() const { return host_; } /// Set the port to listen on @@ -93,22 +88,19 @@ class Service { const std::string& auth_password() const { return auth_pass_; } // ======================================================================== - // AuthMixin (mirrors Python: signalwire.core.mixins.auth_mixin) + // Basic authentication // ======================================================================== /// Validate provided basic-auth credentials against the configured ones /// using a constant-time comparison. - /// Corresponds to ``AuthMixin.validate_basic_auth(username, password)``. [[nodiscard]] bool validate_basic_auth(const std::string& username, const std::string& password) const; - /// Get (user, password) — Python-canonical name. - /// Corresponds to ``AuthMixin.get_basic_auth_credentials``. + /// Get the configured (user, password) pair. [[nodiscard]] std::pair get_basic_auth_credentials() const; /// Get (user, password, source) where source is one of "provided", - /// "environment", or "generated". Corresponds to - /// ``AuthMixin.get_basic_auth_credentials(include_source=True)``. + /// "environment", or "generated". [[nodiscard]] std::tuple get_basic_auth_credentials_with_source() const; @@ -161,8 +153,8 @@ class Service { Service& add_verb(const std::string& section, const std::string& verb_name, const json& params); - /// Add a verb to the main section with strict schema validation (Python: - /// ``SWMLService.add_verb(verb_name, config)``). Validates the config before + /// Add a verb to the main section with strict schema validation. + /// Validates the config before /// appending: an unknown verb, a misspelled/unknown config key, or a /// wrong-typed value throws ``signalwire::utils::SchemaValidationError``. The /// ai (handler) verb is validated by its handler plus a shallow @@ -179,10 +171,9 @@ class Service { Document& document() { return document_; } const Document& document() const { return document_; } - /// SchemaUtils helper bound to this Service. Mirrors Python's - /// `self.schema_utils` instance attribute on `SWMLService`. Built - /// lazily on first access; the underlying schema is cached so the - /// helper is cheap to build. + /// SchemaUtils helper bound to this Service. Built lazily on first + /// access; the underlying schema is cached so the helper is cheap to + /// build. signalwire::utils::SchemaUtils& schema_utils(); const signalwire::utils::SchemaUtils& schema_utils() const; @@ -190,32 +181,26 @@ class Service { [[nodiscard]] json render_swml() const; // ======================================================================== - // Document-lifecycle helpers (Corresponds to SWMLService.*) + // Document-lifecycle helpers // ======================================================================== - /// Add a named section to the document (Python: - /// ``SWMLService.add_section``). Returns ``false`` if the section already - /// exists, ``true`` when a new one is created. + /// Add a named section to the document. Returns ``false`` if the section + /// already exists, ``true`` when a new one is created. bool add_section(const std::string& section_name); - /// Add a verb to a named section (Python: - /// ``SWMLService.add_verb_to_section``). ``config`` is the verb's params - /// object. Returns ``*this`` for fluent chaining. + /// Add a verb to a named section. ``config`` is the verb's params object. + /// Returns ``*this`` for fluent chaining. Service& add_verb_to_section(const std::string& section_name, const std::string& verb_name, const json& config); - /// Return the SWML document as a JSON object (Python: - /// ``SWMLService.get_document`` — a dict). Alias of ``render_swml`` under - /// the Python-canonical name. + /// Return the SWML document as a JSON object. Alias of ``render_swml``. [[nodiscard]] json get_document() const { return render_swml(); } - /// Render the SWML document to a JSON string (Python: - /// ``SWMLService.render_document``). + /// Render the SWML document to a JSON string. [[nodiscard]] std::string render_document() const; - /// Framework-free request-dispatch core (Python: - /// ``SWMLService.handle_request``). The primitive dispatch surface the SDK - /// ports share: over plain ``(method, url, headers, body)`` primitives it + /// Framework-free request-dispatch core. Over plain + /// ``(method, url, headers, body)`` primitives it /// performs proxy detection, basic-auth over the header map, the /// routing-callback check, then renders the SWML document — returning a /// ``(status, response_headers, body_string)`` triple. On auth failure it @@ -227,48 +212,40 @@ class Service { const std::map& headers, const std::optional& body = std::nullopt); - /// Reset the document to an empty state (Python: - /// ``SWMLService.reset_document``). + /// Reset the document to an empty state. void reset_document(); - /// Manually override the proxy URL used to build absolute webhook URLs - /// (Python: ``SWMLService.manual_set_proxy_url``). + /// Manually override the proxy URL used to build absolute webhook URLs. void manual_set_proxy_url(const std::string& proxy_url); - /// Register a routing callback for a request path (Python: - /// ``SWMLService.register_routing_callback``). The callback receives the + /// Register a routing callback for a request path. The callback receives the /// parsed request ``body`` and the request ``headers`` and returns the route - /// to dispatch to (empty string = no override), matching Python's - /// ``callback_fn(body, headers) -> route | None``. + /// to dispatch to (empty string = no override). using RoutingCallback = std::function& headers)>; - void register_routing_callback(RoutingCallback callback, const std::string& path = "/"); + void register_routing_callback(RoutingCallback callback, const std::string& path = "/sip"); - /// The registered (normalized) routing-callback paths, sorted (Python: - /// ``sorted(SWMLService._routing_callbacks.keys())``). + /// The registered (normalized) routing-callback paths, sorted. [[nodiscard]] std::vector get_routing_callback_paths() const; - /// Register a SWML verb handler (Python: - /// ``SWMLService.register_verb_handler``). Delegates to the service's verb - /// handler registry so custom verbs validate + build through the handler. + /// Register a SWML verb handler. Delegates to the service's verb handler + /// registry so custom verbs validate + build through the handler. void register_verb_handler(std::shared_ptr handler); - /// Whether full schema validation of the rendered document is enabled - /// (Python: ``SWMLService.full_validation_enabled``). + /// Whether full schema validation of the rendered document is enabled. [[nodiscard]] bool full_validation_enabled() const { return full_validation_; } - /// Extract the SIP username from a request body's ``call.to`` SIP URI - /// (Python: static ``SWMLService.extract_sip_username``). Returns an empty - /// string when no SIP username can be extracted. + /// Extract the SIP username from a request body's ``call.to`` SIP URI. + /// Returns an empty string when no SIP username can be extracted. [[nodiscard]] static std::string extract_sip_username(const json& request_body); // ======================================================================== // SWAIG tool registry (lifted from AgentBase) // ======================================================================== - /// Define a SWAIG function the AI can call. ``secure`` defaults to TRUE - /// (reference: ``tool_mixin.define_tool(secure=True)``) — a tool defined - /// without an explicit ``secure`` requires SWAIG token validation. + /// Define a SWAIG function the AI can call. ``secure`` defaults to TRUE — a + /// tool defined without an explicit ``secure`` requires SWAIG token + /// validation. Service& define_tool(const std::string& name, const std::string& description, const json& parameters, swaig::ToolHandler handler, bool secure = true); Service& define_tool(const swaig::ToolDefinition& tool); @@ -279,41 +256,39 @@ class Service { /// Dispatch a function call to the registered handler. /// Returns a FunctionResult; if the function isn't registered, returns /// a FunctionResult with a "Function not found" response. + /// ``raw_data`` is OPTIONAL; a JSON null is the absent spelling. [[nodiscard]] virtual swaig::FunctionResult on_function_call(const std::string& name, const json& args, - const json& raw_data); + const json& raw_data = nullptr); [[nodiscard]] bool has_tool(const std::string& name) const; [[nodiscard]] std::vector list_tool_names() const; // ======================================================================== - // ToolRegistry (mirrors Python: signalwire.core.agent.tools.registry) + // Tool registry // ======================================================================== /// Whether a SWAIG function with the given name is registered. - /// Corresponds to ``ToolRegistry.has_function``. [[nodiscard]] bool has_function(const std::string& name) const; /// Get a registered SWAIG function definition by name. /// Returns nullptr when no such function is registered. - /// Corresponds to ``ToolRegistry.get_function``. [[nodiscard]] const swaig::ToolDefinition* get_function(const std::string& name) const; /// Snapshot of all registered SWAIG functions keyed by name. /// Returned by value so subsequent registrations don't mutate the - /// snapshot. Corresponds to ``ToolRegistry.get_all_functions``. + /// snapshot. [[nodiscard]] std::map get_all_functions() const; /// Remove a registered SWAIG function. Returns true when the /// function was found and removed; false when it wasn't registered. - /// Corresponds to ``ToolRegistry.remove_function``. [[nodiscard]] bool remove_function(const std::string& name); /// Build the introspect payload for the registered tools as a JSON string /// shaped like `{"tools":[]}`. Iterates /// `tool_order_` first, falling back to map order for entries registered - /// only via `register_swaig_function`. Stable across SDKs so the - /// `swaig-test --example` CLI can parse output uniformly. Used by the + /// only via `register_swaig_function`. The payload shape is stable so the + /// `swaig-test --example` CLI can parse it. Used by the /// SWAIG_LIST_TOOLS env-var path; pulled out as a separate helper so /// tests can assert content without invoking exit(). [[nodiscard]] std::string build_tool_registry_json() const; @@ -342,11 +317,6 @@ class Service { /// the idiomatic "give me this agent's routes to embed" capability. The /// caller owns the returned Server and can `listen()` on it directly, front /// it behind its own TLS/proxy, or copy its handlers into a parent server. - /// - /// Corresponds to WebMixin.as_router / SWMLService.as_router — the - /// cross-port "embed my routes in a host app" unit (Python returns a FastAPI - /// APIRouter, Go returns an http.Handler; C++ returns a populated - /// httplib::Server). [[nodiscard]] std::shared_ptr as_router(); /// Stop the HTTP server @@ -355,6 +325,36 @@ class Service { /// Get the effective port int port() const { return port_; } + // ======================================================================== + // TLS / serving-domain configuration + // + // These four are seeded from the service's SecurityConfig at construction and + // stay independently settable afterwards. They are caller-observable VALUES, + // so they are contract surface on the service itself — not just on the + // SecurityConfig collaborator. Hence an accessor pair per value, backed by a + // field. + // ======================================================================== + + /// Whether TLS is enabled for this service. + [[nodiscard]] bool ssl_enabled() const { return ssl_enabled_; } + /// Enable/disable TLS, overriding the value seeded from SecurityConfig. + Service& set_ssl_enabled(bool enabled); + + /// The serving domain used to build public URLs. + [[nodiscard]] const std::optional& domain() const { return domain_; } + /// Set the serving domain. + Service& set_domain(const std::string& domain); + + /// TLS certificate path. + [[nodiscard]] const std::optional& ssl_cert_path() const { return ssl_cert_path_; } + /// Set the TLS certificate path. + Service& set_ssl_cert_path(const std::string& path); + + /// TLS private-key path. + [[nodiscard]] const std::optional& ssl_key_path() const { return ssl_key_path_; } + /// Set the TLS private-key path. + Service& set_ssl_key_path(const std::string& path); + /// Timing-safe string comparison using CRYPTO_memcmp [[nodiscard]] static bool timing_safe_compare(const std::string& a, const std::string& b); @@ -368,10 +368,6 @@ class Service { /// Returns std::nullopt to use the default SWML rendering, or a /// non-null JSON with modifications to merge into the rendered /// document. - /// - /// Corresponds to WebMixin.on_request(request_data, callback_path). - /// The Python third `request` argument is FastAPI-specific and - /// intentionally not mirrored on the cross-language API. virtual std::optional on_request( const std::optional& request_data = std::nullopt, const std::optional& callback_path = std::nullopt); @@ -379,10 +375,23 @@ class Service { /// Customization point for subclasses to modify SWML based on /// request data. Default returns std::nullopt (no modification). /// - /// Corresponds to WebMixin.on_swml_request(request_data, callback_path). + /// @param request_data The parsed POST body, or std::nullopt when absent. + /// @param callback_path Optional callback path. + /// @param request The inbound request, modelled as a JSON object carrying + /// `query_params` and `headers` — the reference passes its framework + /// Request object and reads those same two attributes off it. std::nullopt + /// when the call did not originate from a live request: `on_request` passes + /// nothing here, exactly as the reference passes `None` from that path. + /// + /// The third parameter was added 2026-07-30. Without it a C++ subclass + /// overriding this hook could not reach the inbound request AT ALL, so query + /// params and headers were invisible to the dispatch hook — a capability gap + /// against the reference, which keeps its DISPATCH hook request-aware and + /// made only the SECURITY half request-agnostic. virtual std::optional on_swml_request( const std::optional& request_data = std::nullopt, - const std::optional& callback_path = std::nullopt); + const std::optional& callback_path = std::nullopt, + const std::optional& request = std::nullopt); protected: /// Override to customize SWML rendering @@ -422,8 +431,8 @@ class Service { // Mutable so credentials can be lazily resolved from the environment on the // first auth check regardless of entry point (init_auth() is const and called - // by the const validate_basic_auth). Mirrors the reference's lazy auth - // resolution: a serverless dispatch validates auth without a prior serve(). + // by the const validate_basic_auth). Auth resolution must be lazy because a + // serverless dispatch validates auth without a prior serve(). mutable std::string auth_user_; mutable std::string auth_pass_; mutable bool auth_initialized_ = false; @@ -435,20 +444,25 @@ class Service { std::optional manual_proxy_url_; bool full_validation_ = false; - /// Strict schema validation for the 2-arg add_verb (Python: SWMLService - /// schema_validation, default True). + /// Strict schema validation for the 2-arg add_verb. Defaults to true. bool schema_validation_ = true; /// Explicit schema path from the constructor, forwarded to SchemaUtils. - /// Empty optional = let SchemaUtils run its own discovery (the reference's - /// ``_find_schema_path``). + /// Empty optional = let SchemaUtils run its own schema discovery. std::optional schema_path_; /// Config file from the constructor (or auto-discovered for this service /// name), forwarded to SecurityConfig. std::optional config_file_; - // Protected accessors for the three above — the reference's counterparts - // are private (``self._schema_validation``) or not stored at all, so these - // are NOT public surface. + /// The four TLS / domain values, seeded in the ctor from a + /// ``SecurityConfig`` built out of ``config_file_`` + the service name. + /// They stay independently settable afterwards. + bool ssl_enabled_ = false; + std::optional domain_; + std::optional ssl_cert_path_; + std::optional ssl_key_path_; + + // Protected accessors for the three above — deliberately not public surface; + // they exist for subclasses only. [[nodiscard]] bool schema_validation() const { return schema_validation_; } [[nodiscard]] const std::optional& schema_path() const { return schema_path_; } [[nodiscard]] const std::optional& config_file() const { return config_file_; } @@ -467,6 +481,32 @@ class Service { /// the SWML + /swaig base routes. virtual void setup_routes(httplib::Server& server); + protected: + /// The single schema check behind EVERY Service-level verb entry point. + /// + /// Before this existed the Service had three ways in and only one of them + /// consulted the schema: the 2-arg ``add_verb(verb, config)`` validated, while + /// the 3-arg ``add_verb(section, verb, params)`` and + /// ``add_verb_to_section(...)`` — and all 37 of the per-verb convenience + /// methods — delegated straight to the Document. Two of those SHARE the name + /// ``add_verb``, so whether a caller got validation depended purely on arity; + /// nothing at the call site said so. That is how an invalid + /// ``play {"text": ...}`` shipped: the validating path rejects the key, and + /// nothing ever went through the validating path. + /// + /// Throws ``signalwire::utils::SchemaValidationError`` on an unknown verb, a + /// misspelled/unknown top-level key, or a wrong-typed value. A no-op when + /// schema validation is disabled. + /// + /// Protected rather than private so AgentBase — which derives from Service but + /// assembles its rendered document in a LOCAL ``swml::Document`` rather than + /// the Service's own ``document_`` — can validate the verbs it emits through + /// the same check. + /// + /// ``const``: it only reads the schema (the SchemaUtils cache is ``mutable``), + /// so ``AgentBase::render_swml_internal`` — which is const — can call it. + void validate_verb_or_throw(const std::string& verb_name, const json& config) const; + private: void init_auth() const; diff --git a/include/signalwire/utils/schema_utils.hpp b/include/signalwire/utils/schema_utils.hpp index 22a4fc6..23b6e67 100644 --- a/include/signalwire/utils/schema_utils.hpp +++ b/include/signalwire/utils/schema_utils.hpp @@ -1,4 +1,4 @@ -// schema_utils.hpp — C++ port of signalwire.utils.schema_utils.SchemaUtils. +// schema_utils.hpp — SWML schema loading, verb metadata, and validation. // // Loads the SWML JSON Schema, extracts verb metadata, and validates // either a single verb config or a complete SWML document. Validation @@ -6,7 +6,7 @@ // full JSON Schema validation can be wired in via // nlohmann/json-schema-validator by extending init_full_validator. // -// Construction rules mirror Python: +// Construction rules: // // - Pass schema_path = "" to use the embedded schema.json. // - schema_validation = false disables validation @@ -30,8 +30,8 @@ namespace utils { using json = nlohmann::json; -/// SchemaValidationError — C++ port of -/// signalwire.utils.schema_utils.SchemaValidationError. +/// Thrown when a verb config fails schema validation. Carries the verb name and +/// the list of validation errors. class SchemaValidationError : public std::runtime_error { public: SchemaValidationError(std::string verb_name, std::vector errors) @@ -55,52 +55,46 @@ struct VerbInfo { json definition; }; -/// SchemaUtils — C++ port of -/// signalwire.utils.schema_utils.SchemaUtils. +/// Loads the SWML JSON Schema and answers verb-metadata / validation queries +/// against it. class SchemaUtils { public: - /// Construct a SchemaUtils. Mirrors Python's - /// `SchemaUtils(schema_path=None, schema_validation=True)`. - /// Pass schema_path = "" to use the embedded schema. + /// Construct a SchemaUtils. ``schema_path`` defaults to "" — pass "" to use + /// the embedded schema. ``schema_validation`` defaults to true. SchemaUtils(const std::string& schema_path = "", bool schema_validation = true); - /// The schema file path in use (reference: ``self.schema_path``) — the - /// caller-supplied path, or the resolved default when none was given. + /// The schema file path in use — the caller-supplied path, or the resolved + /// default when none was given. /// Empty means the embedded schema is used. [[nodiscard]] const std::string& schema_path() const { return schema_path_; } /// Whether full JSON Schema validation is wired up. - /// Mirrors Python's full_validation_available property. [[nodiscard]] bool full_validation_available() const; - /// Read and parse the JSON Schema. Mirrors Python's load_schema(). + /// Read and parse the JSON Schema. [[nodiscard]] json load_schema(); /// Sorted list of all known verb names. - /// Mirrors Python's get_all_verb_names(). [[nodiscard]] std::vector get_all_verb_names() const; /// The properties[verb_name] block for a verb, or empty when - /// unknown. Mirrors Python's get_verb_properties(verb_name). + /// unknown. [[nodiscard]] json get_verb_properties(const std::string& verb_name) const; /// The required list for a verb, or empty when unknown / not - /// specified. Mirrors Python's get_verb_required_properties(verb_name). + /// specified. [[nodiscard]] std::vector get_verb_required_properties( const std::string& verb_name) const; /// Parameter-definition block used by code-gen tooling. - /// Mirrors Python's get_verb_parameters(verb_name). [[nodiscard]] json get_verb_parameters(const std::string& verb_name) const; /// Validate a verb config against the schema. - /// Mirrors Python's validate_verb(verb_name, verb_config). - /// Returns (valid, errors) — Python's Tuple[bool, List[str]]. + /// Returns (valid, errors). [[nodiscard]] std::pair> validate_verb( const std::string& verb_name, const json& verb_config) const; - /// Validate a complete SWML document. - /// Mirrors Python's validate_document(document). Returns + /// Validate a complete SWML document. Returns /// (false, ["Schema validator not initialized"]) when no full /// validator is wired in. [[nodiscard]] std::pair> validate_document( @@ -111,17 +105,18 @@ class SchemaUtils { /// full deep schema (which would false-reject legitimate deep emissions such /// as the ai verb's empty prompt.pom or SWAIG defaults). Used for handler /// verbs (the ai verb) whose deep shapes the handler owns. A no-op when - /// validation is disabled or when the verb has no enumerable closed key-set. - /// Mirrors Python's SchemaUtils.validate_verb_top_level_keys. + /// validation is disabled or when the verb genuinely has no enumerable closed + /// key-set (an open object such as ``set``, or a union with no object branch + /// such as ``unset``). [[nodiscard]] std::pair> validate_verb_top_level_keys( const std::string& verb_name, const json& verb_config) const; - /// Generate a Python-style method signature string for a verb. - /// Mirrors Python's generate_method_signature(verb_name). + /// Generate a method signature string for a verb, in Python source syntax: + /// a ``self`` receiver, then the verb's parameters sorted by name (optional + /// ones typed ``Optional[...] = None``), then ``**kwargs``. [[nodiscard]] std::string generate_method_signature(const std::string& verb_name) const; - /// Generate a Python-style method body string for a verb. - /// Mirrors Python's generate_method_body(verb_name). + /// Generate a method body string for a verb, in Python source syntax. [[nodiscard]] std::string generate_method_body(const std::string& verb_name) const; private: @@ -133,10 +128,9 @@ class SchemaUtils { const std::string& verb_name, const json& verb_config) const; /// Resolve the set of KNOWN top-level property names for a verb's config - /// object, following a single ``$ref`` (e.g. AI -> AIObject). Returns - /// std::nullopt when the verb's config schema is not a closed - /// object-with-properties (so no shallow key check applies). Mirrors - /// Python's _verb_top_level_property_names. + /// object, following a ``$ref`` (e.g. AI -> AIObject) and UNIONING the branches + /// of an ``anyOf``/``oneOf`` union. Returns std::nullopt only when there is + /// genuinely no enumerable closed key-set (so no shallow key check applies). [[nodiscard]] std::optional> verb_top_level_property_names( const std::string& verb_name) const; diff --git a/include/signalwire/utils/serverless.hpp b/include/signalwire/utils/serverless.hpp index beb8100..5c3df3f 100644 --- a/include/signalwire/utils/serverless.hpp +++ b/include/signalwire/utils/serverless.hpp @@ -20,11 +20,20 @@ namespace utils { using json = nlohmann::json; /** - * Cross-language SDK contract: `signalwire.utils.is_serverless_mode` - * returns `true` whenever the SDK is running inside any short-lived / - * event-driven invocation environment (anything other than `"server"`). - * - * Mirrors `signalwire.utils.is_serverless_mode` in the Python reference. + * Access shim letting the serverless dispatchers reach `AgentBase`'s protected + * `swaig_validate_token` core. Each envelope (lambda / cgi / gcf / azure) + * extracts the credential from its own payload shape and then routes the + * DECISION through that single shared core, so a serverless deployment enforces + * `secure` exactly as the HTTP endpoint does. Declared here (and befriended by + * `AgentBase`) so the enforcement lives in one place instead of being + * re-implemented per transport. + */ +struct ServerlessTokenAccess; + +/** + * Whether the SDK is running inside a short-lived / event-driven invocation + * environment, as opposed to a long-lived server process. Determined from + * `core::logging_config::get_execution_mode()`. * * @return `true` unless the detected mode is `"server"`. */ @@ -33,7 +42,7 @@ using json = nlohmann::json; /** * A platform-neutral serverless response: the `(status, headers, body)` shape * every dispatch handler produces. Lambda/Azure return this as a struct; GCF / - * CGI additionally emit it to stdout when serving live (mirrors php's Adapter). + * CGI additionally emit it to stdout when serving live. */ struct ServerlessResponse { int status = 200; @@ -48,7 +57,6 @@ struct ServerlessResponse { * (HTTP API v2 `rawPath` / `requestContext.http.method`, REST API v1 * `httpMethod` / `path`, base64-decoding `isBase64Encoded` bodies), calls * `agent.handle_request(...)`, and returns the API-Gateway-shaped response. - * Mirrors php `Adapter::handleLambda`. */ [[nodiscard]] ServerlessResponse handle_lambda(agent::AgentBase& agent, const json& event, const json& context = json::object()); @@ -56,8 +64,7 @@ struct ServerlessResponse { /** * Dispatch a Google Cloud Function invocation from an explicit request tuple * (the live GCF path reads these from the runtime; the tuple form is what the - * dispatcher and tests feed in). Calls `agent.handle_request(...)`. Mirrors - * php `Adapter::handleGcf`. + * dispatcher and tests feed in). Calls `agent.handle_request(...)`. */ [[nodiscard]] ServerlessResponse handle_gcf(agent::AgentBase& agent, const std::string& method, const std::string& path, @@ -66,7 +73,7 @@ struct ServerlessResponse { /** * Dispatch an Azure Functions invocation from a request object (method / url / - * headers / body). Mirrors php `Adapter::handleAzure`. + * headers / body). */ [[nodiscard]] ServerlessResponse handle_azure(agent::AgentBase& agent, const json& request); @@ -74,7 +81,7 @@ struct ServerlessResponse { * Dispatch a CGI / FastCGI invocation. Reads REQUEST_METHOD / PATH_INFO / * CONTENT_TYPE / HTTP_* from `env` (defaulting to the process environment when * `env` is empty) and takes the request body explicitly (the live path reads - * it from stdin via CONTENT_LENGTH). Mirrors php `Adapter::handleCgi`. + * it from stdin via CONTENT_LENGTH). */ [[nodiscard]] ServerlessResponse handle_cgi(agent::AgentBase& agent, const std::map& env, @@ -83,8 +90,7 @@ struct ServerlessResponse { /** * Auto-detect (or force) the serverless platform and dispatch the request to * the matching handler, returning the `(status, headers, body)` response. - * Mirrors Python `ServerlessMixin.handle_serverless_request(event, context, - * mode)`: `mode` (empty = auto-detect via get_execution_mode) selects + * `mode` (empty = auto-detect via `get_execution_mode`) selects * lambda / google_cloud_function / azure_function / cgi. An unknown/`"server"` * mode renders SWML via a plain GET `handle_request` so a dispatch always * produces a real response (never a fall-through to serve()). diff --git a/include/signalwire/utils/url_validator.hpp b/include/signalwire/utils/url_validator.hpp index 720e6a4..e17073e 100644 --- a/include/signalwire/utils/url_validator.hpp +++ b/include/signalwire/utils/url_validator.hpp @@ -16,8 +16,8 @@ namespace utils { namespace url_validator { /** - * Cross-port SSRF block list. Order matches the Python reference for - * ease of cross-language review. + * SSRF block list: the CIDR networks a URL's resolved addresses may not + * fall into. */ extern const std::array BLOCKED_NETWORKS; @@ -37,9 +37,6 @@ void _set_resolver(ResolverFn resolver); /** * Validate that a URL is safe to fetch. * - * Mirrors Python's - * ``signalwire.utils.url_validator.validate_url(url, allow_private=False) -> bool``. - * * @param url URL to validate. * @param allow_private When true, bypass the IP-blocklist check. * @return True iff the URL is safe to fetch. diff --git a/include/signalwire/web/web_service.hpp b/include/signalwire/web/web_service.hpp index de8c157..7841e02 100644 --- a/include/signalwire/web/web_service.hpp +++ b/include/signalwire/web/web_service.hpp @@ -3,17 +3,14 @@ // // WebService — static-file serving service over an in-process HTTP server. // -// Mirrors the Python reference signalwire.web.web_service.WebService and the -// Java port com.signalwire.sdk.web.WebService. Maps URL route prefixes to local -// directories and serves their files over HTTP with a file-allowed safety check -// (size + extension/name filters), path-traversal protection, and optional -// basic auth. +// Maps URL route prefixes to local directories and serves their files over HTTP +// with a file-allowed safety check (size + extension/name filters), +// path-traversal protection, and optional basic auth. // -// Idiom note: the Python reference builds a FastAPI/uvicorn app; C++ (an AOT -// port, like Java) uses the vendored cpp-httplib server. start() launches the -// server on a background thread (non-blocking) and returns the bound port, so -// it is safe to start/stop in tests without hanging. Pass port 0 to bind an -// OS-assigned ephemeral port. +// Built on the vendored cpp-httplib server. start() launches the server on a +// background thread (non-blocking) and returns the bound port, so it is safe to +// start/stop in tests without hanging. Pass port 0 to bind an OS-assigned +// ephemeral port. #pragma once @@ -42,9 +39,9 @@ class WebService { /// Construct a WebService. /// - /// Mirrors the Python reference constructor surface. `config_file` is accepted - /// for signature compatibility; config-file loading (SecurityConfig / ConfigLoader) - /// is out of scope for this class and is a no-op here. + /// `config_file` is accepted but currently unused: config-file loading + /// (SecurityConfig / ConfigLoader) is out of scope for this class and is a + /// no-op here. explicit WebService(int port = 8002, std::optional> directories = std::nullopt, std::optional> basic_auth = std::nullopt, @@ -61,7 +58,7 @@ class WebService { /// Add a directory to serve at `route`. Remounts immediately if running. /// Throws std::invalid_argument when the path does not exist or is not a - /// directory (Python raises ValueError). + /// directory. void add_directory(const std::string& route, const std::string& directory); /// Remove the directory served at `route` (no-op when absent). @@ -74,35 +71,31 @@ class WebService { /// Stop the service and release the socket. Safe to call when not running. void stop(); - /// Whether a file may be served (size + extension/name filters). Mirrors the - /// Java fileAllowed / Python _is_file_allowed. + /// Whether a file may be served (size + extension/name filters). [[nodiscard]] bool file_allowed(const std::string& file_path) const; // ---- Accessors ---- - // Every construction parameter the reference keeps as a public instance - // attribute is readable here. A caller hands these in, so a caller must be - // able to read them back (the reference's `self.max_file_size`, - // `self.enable_cors`, … are plain public attributes). + // Every construction parameter is readable back. A caller hands these in, so + // a caller must be able to read them back. [[nodiscard]] int port() const { return port_; } [[nodiscard]] const std::map& directories() const { return directories_; } - /// reference: ``self.enable_directory_browsing`` — whether a directory URL - /// renders a listing instead of 404ing. + /// Whether a directory URL renders a listing instead of 404ing. [[nodiscard]] bool enable_directory_browsing() const { return enable_directory_browsing_; } - /// reference: ``self.allowed_extensions`` — when set, ONLY these extensions - /// are servable (nullopt = no allow-list, all-but-blocked are servable). + /// When set, ONLY these extensions are servable (nullopt = no allow-list, + /// all-but-blocked are servable). [[nodiscard]] const std::optional>& allowed_extensions() const { return allowed_extensions_; } - /// reference: ``self.blocked_extensions`` — never servable; defaulted to the - /// reference's built-in list when the caller passes none. + /// Never servable; defaults to a built-in list (``.env``, ``.git``, ``.key``, + /// ``.pem``, …) when the caller passes none. [[nodiscard]] const std::vector& blocked_extensions() const { return blocked_extensions_; } - /// reference: ``self.max_file_size`` — bytes; a larger file is refused. + /// Maximum servable file size in bytes; a larger file is refused. [[nodiscard]] std::int64_t max_file_size() const { return max_file_size_; } - /// reference: ``self.enable_cors`` — whether CORS headers are emitted. + /// Whether CORS headers are emitted. [[nodiscard]] bool enable_cors() const { return enable_cors_; } private: diff --git a/port_signatures.baseline.json b/port_signatures.baseline.json index e273828..2798a6f 100644 --- a/port_signatures.baseline.json +++ b/port_signatures.baseline.json @@ -1,7 +1,7 @@ { "version": "2", - "generated_from": "signalwire-cpp via libclang", - "generated_from_commit": "9817418e2ade14d13fd96698943eb8e7b45c8f31", + "generated_from": "signalwire-cpp @ 10ef953 (v3.0.0 release floor, commit-anchored) via libclang", + "generated_from_commit": "10ef953a205a7c038558e98e51ac66ba348bb7c7", "modules": { "signalwire": { "functions": { @@ -11,13 +11,13 @@ "name": "args", "type": "list", "required": false, - "default": null + "default": [] }, { "name": "kwargs", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "class:signalwire.rest.client.RestClient" @@ -114,13 +114,19 @@ "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 + }, + { + "name": "log_level", + "type": "string", + "required": false, + "default": "info" } ], "returns": "void" @@ -135,7 +141,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -172,6 +178,15 @@ ], "returns": "dict" }, + "host": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, "list_routes": { "params": [ { @@ -181,6 +196,15 @@ ], "returns": "list" }, + "log_level": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, "lookup_sip_route": { "params": [ { @@ -214,6 +238,15 @@ ], "returns": "class:signalwire.agent_server.AgentServer" }, + "port": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, "register": { "params": [ { @@ -315,7 +348,7 @@ "name": "route", "type": "string", "required": false, - "default": null + "default": "/" } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -344,13 +377,13 @@ "name": "route", "type": "string", "required": false, - "default": null + "default": "/sip" }, { "name": "auto_map", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -410,13 +443,13 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "bedrock_agent" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/bedrock" }, { "name": "system_prompt", @@ -428,25 +461,25 @@ "name": "voice_id", "type": "string", "required": false, - "default": null + "default": "matthew" }, { "name": "temperature", "type": "float", "required": false, - "default": null + "default": 0.7 }, { "name": "top_p", "type": "float", "required": false, - "default": null + "default": 0.9 }, { "name": "max_tokens", "type": "int", "required": false, - "default": null + "default": 1024 } ], "returns": "void" @@ -563,9 +596,9 @@ } } }, - "signalwire.core.agent.prompt.manager": { + "signalwire.ai_chat.client": { "classes": { - "PromptManager": { + "AIChatClient": { "methods": { "__init__": { "params": [ @@ -574,204 +607,207 @@ "kind": "self" }, { - "name": "other", - "type": "class:signalwire.core.agent_base.AgentBase", - "required": true - } - ], - "returns": "void" - }, - "define_contexts": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "class:signalwire.core.contexts.ContextBuilder" - }, - "get_contexts": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, - "get_post_prompt": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, - "get_prompt": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, - "get_raw_prompt": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, - "prompt_add_section": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "project", + "type": "optional", + "required": false, + "default": null }, { - "name": "title", - "type": "string", - "required": true + "name": "token", + "type": "optional", + "required": false, + "default": null }, { - "name": "body", - "type": "string", + "name": "space", + "type": "optional", "required": false, "default": null }, { - "name": "bullets", - "type": "list", + "name": "url", + "type": "optional", "required": false, "default": null } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "void" }, - "prompt_add_subsection": { + "chat": { "params": [ { "name": "self", "kind": "self" }, { - "name": "parent_title", + "name": "conversation_id", "type": "string", "required": true }, { - "name": "title", + "name": "message", "type": "string", "required": true }, { - "name": "body", + "name": "role", "type": "string", "required": false, + "default": "user" + }, + { + "name": "config_url", + "type": "optional", + "required": false, "default": null }, { - "name": "bullets", - "type": "list", + "name": "user_metadata", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "timeout", + "type": "optional", "required": false, "default": null + }, + { + "name": "reinit", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "class:signalwire.ai_chat.client.ChatResponse" }, - "prompt_add_to_section": { + "close": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "create_conversation": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "conversation_id", "type": "string", "required": true }, { - "name": "body", + "name": "config_url", "type": "string", + "required": true + }, + { + "name": "user_message", + "type": "optional", "required": false, "default": null }, { - "name": "bullets", - "type": "list", + "name": "timeout", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "user_metadata", + "type": "optional>", "required": false, "default": null + }, + { + "name": "reinit", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "class:signalwire.ai_chat.client.ConversationInfo" }, - "prompt_has_section": { + "delete": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "conversation_id", "type": "string", "required": true } ], "returns": "bool" }, - "set_post_prompt": { + "end": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", + "name": "conversation_id", "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "bool" }, - "set_prompt_pom": { + "log": { "params": [ { "name": "self", "kind": "self" }, { - "name": "pom", - "type": "list", + "name": "conversation_id", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "class:signalwire.ai_chat.client.ChatLog" }, - "set_prompt_text": { + "summarize": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", + "name": "conversation_id", "type": "string", "required": true + }, + { + "name": "summary_prompt", + "type": "optional", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + "returns": "string" + }, + "url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" } } - } - } - }, - "signalwire.core.agent.tools.registry": { - "classes": { - "ToolRegistry": { + }, + "AIChatError": { "methods": { "__init__": { "params": [ @@ -780,866 +816,765 @@ "kind": "self" }, { - "name": "other", - "type": "class:signalwire.core.agent_base.AgentBase", + "name": "code", + "type": "optional", + "required": true + }, + { + "name": "message", + "type": "string", "required": true } ], "returns": "void" }, - "define_tool": { + "code": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "tool", - "type": "class:signalwire.core.swaig_function.ToolDefinition", - "required": true } ], - "returns": "class:signalwire.core.agent.tools.registry.ToolRegistry" + "returns": "any" }, - "get_all_functions": { + "message": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "dict" - }, - "get_function": { + "returns": "any" + } + } + }, + "ChatLog": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "messages", + "type": "list>", + "required": false, + "default": "list()" + }, + { + "name": "call_timeline", + "type": "list>", + "required": false, + "default": "list()" } ], - "returns": "class:signalwire.core.swaig_function.ToolDefinition" + "returns": "void" }, - "has_function": { + "call_timeline": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - } - ], - "returns": "bool" - }, - "register_swaig_function": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "func_def", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.agent.tools.registry.ToolRegistry" + "returns": "any" }, - "remove_function": { + "messages": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" } } - } - } - }, - "signalwire.core.agent.tools.type_inference": { - "functions": { - "create_typed_handler_wrapper": { - "params": [ - { - "name": "func", - "type": "callable,class:signalwire.core.function_result.FunctionResult>", - "required": true - }, - { - "name": "has_raw_data", - "type": "bool", - "required": true - } - ], - "returns": "class:signalwire.tool_handler.ToolHandler" }, - "infer_schema": { - "params": [ - { - "name": "params", - "type": "class:signalwire.swaig.parameter_schema.ParameterSchema", - "required": true - }, - { - "name": "description", - "type": "optional", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.inferred_schema.InferredSchema" - } - } - }, - "signalwire.core.agent_base": { - "classes": { - "AgentBase": { + "ChatResponse": { "methods": { - "add_answer_verb": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", + "name": "text", "type": "string", "required": true }, { - "name": "params", - "type": "any", + "name": "conversation_id", + "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "add_context": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "user_event", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "void" }, - "add_post_ai_verb": { + "conversation_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "add_post_answer_verb": { + "text": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "add_pre_answer_verb": { + "user_event": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "add_swaig_query_param": { + "returns": "any" + } + } + }, + "ConversationInfo": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "key", + "name": "id", "type": "string", "required": true }, { - "name": "value", + "name": "status", "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "add_swaig_query_params": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "auth_password": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, - "auth_username": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, - "auto_map_sip_usernames": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "enable", - "type": "bool", + "name": "initial_message", + "type": "optional", "required": false, "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "build_mcp_tool_list": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "list" + "returns": "void" }, - "clear_post_ai_verbs": { + "id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "clear_post_answer_verbs": { + "initial_message": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "clear_pre_answer_verbs": { + "status": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "clear_swaig_query_params": { + "returns": "any" + } + } + } + } + }, + "signalwire.core.agent.prompt.manager": { + "classes": { + "PromptManager": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "create_tool_token": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "name", + "type": "string", + "required": false, + "default": "agent" }, { - "name": "tool_name", + "name": "route", "type": "string", - "required": true + "required": false, + "default": "/" }, { - "name": "call_id", + "name": "host", "type": "string", - "required": true - } - ], - "returns": "string" - }, - "enable_sip_routing": { - "params": [ + "required": false, + "default": "0.0.0.0" + }, { - "name": "self", - "kind": "self" + "name": "port", + "type": "optional", + "required": false, + "default": null }, { - "name": "enable", - "type": "bool", + "name": "basic_auth", + "type": "optional>", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "get_full_url": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "use_pom", + "type": "bool", + "required": false, + "default": true }, { - "name": "include_auth", + "name": "token_expiry_secs", + "type": "int", + "required": false, + "default": 3600 + }, + { + "name": "auto_answer", "type": "bool", "required": false, - "default": null - } - ], - "returns": "string" - }, - "get_global_data": { - "params": [ + "default": true + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "get_name": { - "params": [ + "name": "record_call", + "type": "bool", + "required": false, + "default": false + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, - "get_sip_usernames": { - "params": [ + "name": "record_format", + "type": "string", + "required": false, + "default": "mp4" + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "list" - }, - "handle_mcp_request": { - "params": [ + "name": "record_stereo", + "type": "bool", + "required": false, + "default": true + }, { - "name": "self", - "kind": "self" + "name": "default_webhook_url", + "type": "optional", + "required": false, + "default": null }, { - "name": "body", - "type": "any", - "required": true - } - ], - "returns": "any" - }, - "handle_request": { - "params": [ + "name": "agent_id", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "native_functions", + "type": "optional>", + "required": false, + "default": null }, { - "name": "method", - "type": "string", - "required": true + "name": "schema_path", + "type": "optional", + "required": false, + "default": null }, { - "name": "url", - "type": "string", - "required": true + "name": "suppress_logs", + "type": "bool", + "required": false, + "default": false }, { - "name": "headers", - "type": "dict", - "required": true + "name": "enable_post_prompt_override", + "type": "bool", + "required": false, + "default": false }, { - "name": "body", - "type": "optional", + "name": "check_for_input_override", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "config_file", + "type": "optional", "required": false, "default": null - } - ], - "returns": "tuple,string>" - }, - "has_contexts": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "signing_key", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "trust_proxy_for_signature", + "type": "bool", + "required": false, + "default": false } ], - "returns": "bool" + "returns": "void" }, - "is_mcp_server_enabled": { + "agent": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "list_tools": { + "define_contexts": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "class:signalwire.core.contexts.ContextBuilder" }, - "mcp_servers": { + "get_contexts": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "optional" }, - "on_debug_event": { + "get_post_prompt": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "callable,any>", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "optional" }, - "on_summary": { + "get_prompt": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:Callable", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "string" }, - "pom": { + "get_raw_prompt": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "optional" }, - "register_sip_username": { + "prompt_add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "username", + "name": "title", "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "render_swml": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "body", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": [] } ], - "returns": "any" + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" }, - "render_swml_for_request": { + "prompt_add_subsection": { "params": [ { "name": "self", "kind": "self" }, { - "name": "query_params", - "type": "dict", + "name": "parent_title", + "type": "string", "required": true }, { - "name": "body_params", - "type": "any", + "name": "title", + "type": "string", "required": true }, { - "name": "headers", - "type": "dict", - "required": true - } - ], - "returns": "any" - }, - "session_manager": { - "params": [ + "name": "body", + "type": "string", + "required": false, + "default": "" + }, { - "name": "self", - "kind": "self" + "name": "bullets", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.security.session_manager.SessionManager" + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" }, - "set_auth": { + "prompt_add_to_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "username", + "name": "title", "type": "string", "required": true }, { - "name": "password", + "name": "body", "type": "string", - "required": true + "required": false, + "default": null + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": [] } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" }, - "set_name": { + "prompt_has_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "n", + "name": "title", "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "bool" }, - "set_post_prompt_url": { + "set_post_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "url", + "name": "text", "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" }, - "set_post_prompt_url_direct": { + "set_prompt_pom": { "params": [ { "name": "self", "kind": "self" }, { - "name": "url", - "type": "string", + "name": "pom", + "type": "list", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" }, - "set_signing_key": { + "set_prompt_text": { "params": [ { "name": "self", "kind": "self" }, { - "name": "key", + "name": "text", "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_use_pom": { + "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" + } + } + } + } + }, + "signalwire.core.agent.tools.registry": { + "classes": { + "ToolRegistry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "name", + "type": "string", + "required": false, + "default": "agent" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "basic_auth", + "type": "optional>", + "required": false, + "default": null + }, { "name": "use_pom", "type": "bool", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_web_hook_url": { - "params": [ + "required": false, + "default": true + }, { - "name": "self", - "kind": "self" + "name": "token_expiry_secs", + "type": "int", + "required": false, + "default": 3600 }, { - "name": "url", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_webhook_url": { - "params": [ + "name": "auto_answer", + "type": "bool", + "required": false, + "default": true + }, { - "name": "self", - "kind": "self" + "name": "record_call", + "type": "bool", + "required": false, + "default": false }, { - "name": "url", + "name": "record_format", "type": "string", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "signing_key": { - "params": [ + "required": false, + "default": "mp4" + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, - "stop": { - "params": [ + "name": "record_stereo", + "type": "bool", + "required": false, + "default": true + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "void" - }, - "supported_internal_filler_names": { - "params": [], - "returns": "list" - }, - "trust_proxy_for_signature": { - "params": [ + "name": "default_webhook_url", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "agent_id", + "type": "optional", + "required": false, + "default": null }, { - "name": "trust", + "name": "native_functions", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "schema_path", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "suppress_logs", "type": "bool", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - } - } - } - } - }, - "signalwire.core.auth_exception": { - "classes": { - "AuthException": { - "methods": { - "__init__": { - "params": [ + "required": false, + "default": false + }, { - "name": "self", - "kind": "self" + "name": "enable_post_prompt_override", + "type": "bool", + "required": false, + "default": false }, { - "name": "response", - "type": "class:signalwire.core.auth_response.AuthResponse", - "required": true + "name": "check_for_input_override", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "signing_key", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "trust_proxy_for_signature", + "type": "bool", + "required": false, + "default": false } ], "returns": "void" }, - "response": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "class:signalwire.core.auth_response.AuthResponse" - } - } - } - } - }, - "signalwire.core.auth_handler": { - "classes": { - "AuthHandler": { - "methods": { - "__init__": { + "agent": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "security_config", - "type": "class:signalwire.core.security_config.SecurityConfig", - "required": true } ], - "returns": "void" + "returns": "any" }, - "flask_decorator": { + "define_tool": { "params": [ { "name": "self", "kind": "self" }, { - "name": "app", - "type": "callable>,class:signalwire.core.auth_response.AuthResponse>", + "name": "tool", + "type": "class:signalwire.core.swaig_function.ToolDefinition", "required": true } ], - "returns": "callable>,class:signalwire.core.auth_response.AuthResponse>" + "returns": "class:signalwire.core.agent.tools.registry.ToolRegistry" }, - "get_auth_info": { + "get_all_functions": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "dict" }, - "get_fastapi_dependency": { + "get_function": { "params": [ { "name": "self", "kind": "self" }, { - "name": "optional", - "type": "bool", - "required": false, - "default": null + "name": "name", + "type": "string", + "required": true } ], - "returns": "callable>,class:signalwire.core.auth_result.AuthResult>" + "returns": "class:signalwire.core.swaig_function.ToolDefinition" }, - "verify_api_key": { + "has_function": { "params": [ { "name": "self", "kind": "self" }, { - "name": "api_key", + "name": "name", "type": "string", "required": true } ], "returns": "bool" }, - "verify_basic_auth": { + "register_swaig_function": { "params": [ { "name": "self", "kind": "self" }, { - "name": "credentials", - "type": "class:signalwire.core.basic_credentials.BasicCredentials", + "name": "func_def", + "type": "any", "required": true } ], - "returns": "bool" + "returns": "class:signalwire.core.agent.tools.registry.ToolRegistry" }, - "verify_bearer_token": { + "remove_function": { "params": [ { "name": "self", "kind": "self" }, { - "name": "credentials", - "type": "class:signalwire.core.bearer_credentials.BearerCredentials", + "name": "name", + "type": "string", "required": true } ], @@ -1649,570 +1584,535 @@ } } }, - "signalwire.core.config_loader": { - "classes": { - "ConfigLoader": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "config_paths", - "type": "optional>", - "required": false, - "default": null - } - ], - "returns": "void" - }, - "find_config_file": { - "params": [ - { - "name": "service_name", - "type": "optional", + "signalwire.core.agent.tools.type_inference": { + "functions": { + "create_typed_handler_wrapper": { + "params": [ + { + "name": "func", + "type": "callable,class:signalwire.core.function_result.FunctionResult>", + "required": true + }, + { + "name": "has_raw_data", + "type": "bool", + "required": true + } + ], + "returns": "class:signalwire.tool_handler.ToolHandler" + }, + "infer_schema": { + "params": [ + { + "name": "params", + "type": "class:signalwire.swaig.parameter_schema.ParameterSchema", + "required": true + }, + { + "name": "description", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.inferred_schema.InferredSchema" + } + } + }, + "signalwire.core.agent_base": { + "classes": { + "AgentBase": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": false, + "default": "agent" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "optional", "required": false, "default": null }, { - "name": "additional_paths", - "type": "optional>", + "name": "basic_auth", + "type": "optional>", "required": false, "default": null - } - ], - "returns": "optional" - }, - "get": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "use_pom", + "type": "bool", + "required": false, + "default": true }, { - "name": "key_path", + "name": "token_expiry_secs", + "type": "int", + "required": false, + "default": 3600 + }, + { + "name": "auto_answer", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "record_call", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "record_format", "type": "string", - "required": true + "required": false, + "default": "mp4" }, { - "name": "default_value", - "type": "any", + "name": "record_stereo", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "default_webhook_url", + "type": "optional", "required": false, "default": null - } - ], - "returns": "any" - }, - "get_config": { - "params": [ + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "get_config_file": { - "params": [ + "name": "agent_id", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, - "get_section": { - "params": [ + "name": "native_functions", + "type": "optional>", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "schema_path", + "type": "optional", + "required": false, + "default": null }, { - "name": "section", - "type": "string", - "required": true - } - ], - "returns": "any" - }, - "has_config": { - "params": [ + "name": "suppress_logs", + "type": "bool", + "required": false, + "default": false + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "bool" - }, - "merge_with_env": { - "params": [ + "name": "enable_post_prompt_override", + "type": "bool", + "required": false, + "default": false + }, { - "name": "self", - "kind": "self" + "name": "check_for_input_override", + "type": "bool", + "required": false, + "default": false }, { - "name": "env_prefix", - "type": "string", + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "signing_key", + "type": "optional", "required": false, "default": null + }, + { + "name": "trust_proxy_for_signature", + "type": "bool", + "required": false, + "default": false } ], - "returns": "any" + "returns": "void" }, - "substitute_vars": { + "add_answer_verb": { "params": [ { "name": "self", "kind": "self" }, { - "name": "value", - "type": "any", + "name": "verb_name", + "type": "string", "required": true }, { - "name": "max_depth", - "type": "int", - "required": false, - "default": null + "name": "params", + "type": "any", + "required": true } ], - "returns": "any" - } - } - } - } - }, - "signalwire.core.contexts": { - "classes": { - "Context": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "add_context": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.contexts.Context" }, - "add_bullets": { + "add_post_ai_verb": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "verb_name", "type": "string", "required": true }, { - "name": "bullets", - "type": "list", + "name": "params", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_enter_filler": { + "add_post_answer_verb": { "params": [ { "name": "self", "kind": "self" }, { - "name": "lang", + "name": "verb_name", "type": "string", "required": true }, { - "name": "fillers", - "type": "list", + "name": "params", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_exit_filler": { + "add_pre_answer_verb": { "params": [ { "name": "self", "kind": "self" }, { - "name": "lang", + "name": "verb_name", "type": "string", "required": true }, { - "name": "fillers", - "type": "list", + "name": "params", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_section": { + "add_swaig_query_param": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "key", "type": "string", "required": true }, { - "name": "body", + "name": "value", "type": "string", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_step": { + "add_swaig_query_params": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "params", + "type": "any", "required": true - }, - { - "name": "task", - "type": "string", - "required": false, - "default": null - }, - { - "name": "bullets", - "type": "list", - "required": false, - "default": null - }, - { - "name": "criteria", - "type": "string", - "required": false, - "default": null - }, - { - "name": "functions", - "type": "optional>>", - "required": false, - "default": null - }, - { - "name": "valid_steps", - "type": "list", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_system_bullets": { + "agent_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "title", - "type": "string", - "required": true - }, - { - "name": "bullets", - "type": "list", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "string" }, - "add_system_section": { + "auth_password": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "title", - "type": "string", - "required": true - }, - { - "name": "body", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "string" }, - "get_step": { + "auth_username": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "string" }, - "has_steps": { + "auto_map_sip_usernames": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "enable", + "type": "bool", + "required": false, + "default": true } ], - "returns": "bool" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "initial_step": { + "build_mcp_tool_list": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "list" }, - "move_step": { + "clear_post_ai_verbs": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "position", - "type": "int", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "name": { + "clear_post_answer_verbs": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "remove_step": { + "clear_pre_answer_verbs": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "set_consolidate": { + "clear_swaig_query_params": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "c", - "type": "bool", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "set_enter_fillers": { + "create_tool_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fillers", - "type": "any", + "name": "tool_name", + "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.contexts.Context" - }, - "set_exit_fillers": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "fillers", - "type": "any", + "name": "call_id", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "string" }, - "set_full_reset": { + "enable_sip_routing": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fr", + "name": "enable", "type": "bool", - "required": true + "required": false, + "default": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "set_history": { + "get_full_url": { "params": [ { "name": "self", "kind": "self" }, { - "name": "history", - "type": "string", - "required": true + "name": "include_auth", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "string" }, - "set_initial_step": { + "get_global_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "step_name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "any" }, - "set_isolated": { + "get_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "isolated", - "type": "bool", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "string" }, - "set_post_prompt": { + "get_sip_usernames": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "pp", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "list" }, - "set_prompt": { + "handle_mcp_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "prompt", - "type": "string", + "name": "body", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "any" }, - "set_system_prompt": { + "handle_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "sp", + "name": "method", "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.contexts.Context" - }, - "set_user_prompt": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "up", + "name": "url", "type": "string", "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true + }, + { + "name": "body", + "type": "optional", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "tuple,string>" }, - "set_valid_contexts": { + "has_contexts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "ctxs", - "type": "list", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "bool" }, - "set_valid_steps": { + "is_mcp_server_enabled": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "steps", - "type": "list", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "bool" }, - "step_order": { + "list_tools": { "params": [ { "name": "self", @@ -2221,235 +2121,263 @@ ], "returns": "list" }, - "steps": { + "mcp_servers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "dict" + "returns": "list" }, - "to_json": { + "native_functions": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "list" }, - "valid_contexts": { + "on_debug_event": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "optional>" - } - } - }, - "ContextBuilder": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "cb", + "type": "callable,any>", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_context": { + "on_summary": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "cb", + "type": "class:Callable", "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_context": { + "pom": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Context" + "returns": "optional" }, - "has_contexts": { + "register_sip_username": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "username", + "type": "string", + "required": true } ], - "returns": "bool" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "reset": { + "render_swml": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.contexts.ContextBuilder" + "returns": "any" }, - "to_json": { + "render_swml_for_request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "query_params", + "type": "dict", + "required": true + }, + { + "name": "body_params", + "type": "any", + "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true } ], "returns": "any" }, - "validate": { + "session_manager": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" - } - } - }, - "GatherInfo": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.security.session_manager.SessionManager" + }, + "set_auth": { "params": [ { "name": "self", "kind": "self" }, { - "name": "output_key", + "name": "username", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "completion_action", + "name": "password", "type": "string", - "required": false, - "default": null - }, + "required": true + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_name": { + "params": [ { - "name": "prompt", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "isolated", - "type": "bool", - "required": false, - "default": null + "name": "n", + "type": "string", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_question": { + "set_post_prompt_url": { "params": [ { "name": "self", "kind": "self" }, { - "name": "key", + "name": "url", "type": "string", "required": true + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_post_prompt_url_direct": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "question", + "name": "url", "type": "string", "required": true - }, + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_use_pom": { + "params": [ { - "name": "type", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "confirm", + "name": "use_pom", "type": "bool", - "required": false, - "default": null - }, - { - "name": "prompt", - "type": "string", - "required": false, - "default": null - }, + "required": true + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_web_hook_url": { + "params": [ { - "name": "functions", - "type": "list", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "isolated", - "type": "optional", - "required": false, - "default": null + "name": "url", + "type": "string", + "required": true } ], - "returns": "class:signalwire.core.contexts.GatherInfo" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "completion_action": { + "set_webhook_url": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "url", + "type": "string", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "has_questions": { + "signing_key": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "optional" }, - "questions": { + "stop": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "to_json": { + "supported_internal_filler_names": { + "params": [], + "returns": "list" + }, + "trust_proxy_for_signature": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "trust", + "type": "bool", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" } } - }, - "GatherQuestion": { + } + } + }, + "signalwire.core.auth_exception": { + "classes": { + "AuthException": { "methods": { "__init__": { "params": [ @@ -2458,2396 +2386,2472 @@ "kind": "self" }, { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "question", - "type": "string", + "name": "response", + "type": "class:signalwire.core.auth_response.AuthResponse", "required": true - }, - { - "name": "type", - "type": "string", - "required": false, - "default": null - }, - { - "name": "confirm", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "prompt", - "type": "string", - "required": false, - "default": null - }, - { - "name": "functions", - "type": "list", - "required": false, - "default": null - }, - { - "name": "isolated", - "type": "optional", - "required": false, - "default": null } ], "returns": "void" }, - "key": { + "response": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "to_json": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" + "returns": "class:signalwire.core.auth_response.AuthResponse" } } - }, - "Step": { + } + } + }, + "signalwire.core.auth_handler": { + "classes": { + "AuthHandler": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "security_config", + "type": "class:signalwire.core.security_config.SecurityConfig", + "required": true } ], "returns": "void" }, - "add_bullets": { + "flask_decorator": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", - "type": "string", - "required": true - }, - { - "name": "bullets", - "type": "list", + "name": "app", + "type": "callable>,class:signalwire.core.auth_response.AuthResponse>", "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "callable>,class:signalwire.core.auth_response.AuthResponse>" }, - "add_gather_question": { + "get_auth_info": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "question", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "get_fastapi_dependency": { + "params": [ { - "name": "type", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "confirm", + "name": "optional", "type": "bool", "required": false, - "default": null - }, - { - "name": "prompt", - "type": "string", - "required": false, - "default": null - }, - { - "name": "functions", - "type": "list", - "required": false, - "default": null - }, + "default": false + } + ], + "returns": "callable>,class:signalwire.core.auth_result.AuthResult>" + }, + "security_config": { + "params": [ { - "name": "isolated", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.security_config.SecurityConfig" }, - "add_section": { + "verify_api_key": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "api_key", "type": "string", "required": true + } + ], + "returns": "bool" + }, + "verify_basic_auth": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "body", - "type": "string", + "name": "credentials", + "type": "class:signalwire.core.auth_handler.BasicCredentials", "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "bool" }, - "clear_sections": { + "verify_bearer_token": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "credentials", + "type": "class:signalwire.core.auth_handler.BearerCredentials", + "required": true } ], - "returns": "class:signalwire.core.contexts.Step" - }, - "gather_info": { + "returns": "bool" + } + } + }, + "BasicCredentials": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "void" }, - "name": { + "password": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "set_end": { + "username": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + } + } + }, + "BearerCredentials": { + "methods": { + "__init__": { + "params": [ { - "name": "end", - "type": "bool", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "void" }, - "set_functions": { + "credentials": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "functions", - "type": "union>", - "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" }, - "set_gather_info": { + "scheme": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.core.config_loader": { + "classes": { + "ConfigLoader": { + "methods": { + "__init__": { + "params": [ { - "name": "output_key", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "completion_action", - "type": "string", + "name": "config_paths", + "type": "optional>", "required": false, "default": null - }, + } + ], + "returns": "void" + }, + "config_paths": { + "params": [ { - "name": "prompt", - "type": "string", + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "find_config_file": { + "params": [ + { + "name": "service_name", + "type": "optional", "required": false, "default": null }, { - "name": "isolated", - "type": "bool", + "name": "additional_paths", + "type": "optional>", "required": false, "default": null } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "optional" }, - "set_history": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "history", + "name": "key_path", "type": "string", "required": true + }, + { + "name": "default_value", + "type": "any", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" }, - "set_reset_consolidate": { + "get_config": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "c", - "type": "bool", - "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" }, - "set_reset_full_reset": { + "get_config_file": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "fr", - "type": "bool", - "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "optional" }, - "set_reset_system_prompt": { + "get_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "sp", + "name": "section", "type": "string", "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" }, - "set_reset_user_prompt": { + "has_config": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "up", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "bool" }, - "set_skip_to_next_step": { + "merge_with_env": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skip", - "type": "bool", - "required": true + "name": "env_prefix", + "type": "string", + "required": false, + "default": "SWML_" } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" }, - "set_skip_user_turn": { + "substitute_vars": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skip", - "type": "bool", + "name": "value", + "type": "any", "required": true + }, + { + "name": "max_depth", + "type": "int", + "required": false, + "default": 10 } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "any" + } + } + } + } + }, + "signalwire.core.contexts": { + "classes": { + "Context": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "set_step_criteria": { + "add_bullets": { "params": [ { "name": "self", "kind": "self" }, { - "name": "criteria", + "name": "title", "type": "string", "required": true + }, + { + "name": "bullets", + "type": "list", + "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.contexts.Context" }, - "set_text": { + "add_enter_filler": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", + "name": "lang", "type": "string", "required": true + }, + { + "name": "fillers", + "type": "list", + "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.contexts.Context" }, - "set_valid_contexts": { + "add_exit_filler": { "params": [ { "name": "self", "kind": "self" }, { - "name": "ctxs", + "name": "lang", + "type": "string", + "required": true + }, + { + "name": "fillers", "type": "list", "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.contexts.Context" }, - "set_valid_steps": { + "add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "steps", - "type": "list", + "name": "title", + "type": "string", + "required": true + }, + { + "name": "body", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.contexts.Step" + "returns": "class:signalwire.core.contexts.Context" }, - "to_json": { + "add_step": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "task", + "type": "string", + "required": false, + "default": null + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": null + }, + { + "name": "criteria", + "type": "string", + "required": false, + "default": null + }, + { + "name": "functions", + "type": "optional>>", + "required": false, + "default": null + }, + { + "name": "valid_steps", + "type": "list", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.contexts.Step" }, - "valid_contexts": { + "add_system_bullets": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "title", + "type": "string", + "required": true + }, + { + "name": "bullets", + "type": "list", + "required": true } ], - "returns": "optional>" + "returns": "class:signalwire.core.contexts.Context" }, - "valid_steps": { + "add_system_section": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "title", + "type": "string", + "required": true + }, + { + "name": "body", + "type": "string", + "required": true } ], - "returns": "optional>" - } - } - } - } - }, - "signalwire.core.data_map": { - "classes": { - "DataMap": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.contexts.Context" + }, + "get_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_name", + "name": "name", "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.contexts.Step" }, - "body": { + "has_steps": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "bool" + }, + "initial_step": { + "params": [ { - "name": "data", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "optional" }, - "description": { + "move_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "desc", + "name": "name", "type": "string", "required": true + }, + { + "name": "position", + "type": "int", + "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "error_keys": { + "name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "keys", - "type": "list", - "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "string" }, - "expression": { + "remove_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "test_value", + "name": "name", "type": "string", "required": true - }, + } + ], + "returns": "class:signalwire.core.contexts.Context" + }, + "set_consolidate": { + "params": [ { - "name": "pattern", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "output", - "type": "class:signalwire.core.function_result.FunctionResult", + "name": "c", + "type": "bool", "required": true - }, - { - "name": "nomatch_output", - "type": "class:signalwire.core.function_result.FunctionResult", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "fallback_output": { + "set_enter_fillers": { "params": [ { "name": "self", "kind": "self" }, { - "name": "result", - "type": "class:signalwire.core.function_result.FunctionResult", + "name": "fillers", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "foreach": { + "set_exit_fillers": { "params": [ { "name": "self", "kind": "self" }, { - "name": "foreach_config", + "name": "fillers", "type": "any", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "global_error_keys": { + "set_full_reset": { "params": [ { "name": "self", "kind": "self" }, { - "name": "keys", - "type": "list", + "name": "fr", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "output": { + "set_history": { "params": [ { "name": "self", "kind": "self" }, { - "name": "result", - "type": "class:signalwire.core.function_result.FunctionResult", + "name": "history", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "parameter": { + "set_initial_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "step_name", "type": "string", "required": true - }, - { - "name": "param_type", - "type": "string", - "required": true - }, - { - "name": "desc", - "type": "string", - "required": true - }, - { - "name": "required", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "enum_values", - "type": "list", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "params": { + "set_isolated": { "params": [ { "name": "self", "kind": "self" }, { - "name": "data", - "type": "any", + "name": "isolated", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "purpose": { + "set_post_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "desc", + "name": "pp", "type": "string", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "to_swaig_function": { + "set_prompt": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "prompt", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.contexts.Context" }, - "webhook": { + "set_system_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "url", + "name": "sp", "type": "string", "required": true - }, - { - "name": "headers", - "type": "any", - "required": false, - "default": null - }, - { - "name": "form_param", - "type": "string", - "required": false, - "default": null - }, - { - "name": "input_args_as_params", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "require_args", - "type": "list", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.data_map.DataMap" + "returns": "class:signalwire.core.contexts.Context" }, - "webhook_expressions": { + "set_user_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "expressions", - "type": "list", + "name": "up", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.data_map.DataMap" - } - } - } - } - }, - "signalwire.core.function_result": { - "classes": { - "FunctionResult": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.contexts.Context" + }, + "set_valid_contexts": { "params": [ { "name": "self", "kind": "self" }, { - "name": "response", - "type": "string", - "required": false, - "default": null - }, - { - "name": "post_process", - "type": "bool", - "required": false, - "default": null + "name": "ctxs", + "type": "list", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.contexts.Context" }, - "add_action": { + "set_valid_steps": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "data", - "type": "any", + "name": "steps", + "type": "list", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Context" }, - "add_actions": { + "step_order": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "actions", - "type": "list", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "list" }, - "add_dynamic_hints": { + "steps": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "hints", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "dict" }, - "clear_dynamic_hints": { + "to_json": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "connect": { + "valid_contexts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "destination", - "type": "string", - "required": true - }, - { - "name": "final", - "type": "bool", - "required": false, - "default": null - }, + } + ], + "returns": "optional>" + } + } + }, + "ContextBuilder": { + "methods": { + "__init__": { + "params": [ { - "name": "from_addr", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "create_payment_action": { + "add_context": { "params": [ { - "name": "action_type", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "phrase", + "name": "name", "type": "string", "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.contexts.Context" }, - "create_payment_parameter": { + "get_context": { "params": [ { - "name": "name", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "value", + "name": "name", "type": "string", "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.contexts.Context" }, - "create_payment_prompt": { + "has_contexts": { "params": [ { - "name": "for_situation", - "type": "string", - "required": true - }, - { - "name": "actions", - "type": "list", - "required": true - }, - { - "name": "card_type", - "type": "string", - "required": false, - "default": null - }, - { - "name": "error_type", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" }, - "enable_extensive_data": { + "reset": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "enabled", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.ContextBuilder" }, - "enable_functions_on_timeout": { + "to_json": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "enabled", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "execute_rpc": { + "validate": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + } + } + }, + "GatherInfo": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", + "name": "output_key", + "type": "optional", "required": false, "default": null }, { - "name": "call_id", - "type": "string", + "name": "completion_action", + "type": "optional", "required": false, "default": null }, { - "name": "node_id", - "type": "string", + "name": "prompt", + "type": "optional", "required": false, "default": null + }, + { + "name": "isolated", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "execute_swml": { + "add_question": { "params": [ { "name": "self", "kind": "self" }, { - "name": "swml_content", - "type": "any", + "name": "key", + "type": "string", "required": true }, { - "name": "transfer", + "name": "question", + "type": "string", + "required": true + }, + { + "name": "type", + "type": "string", + "required": false, + "default": "string" + }, + { + "name": "confirm", "type": "bool", "required": false, + "default": false + }, + { + "name": "prompt", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "functions", + "type": "list", + "required": false, + "default": null + }, + { + "name": "isolated", + "type": "optional", + "required": false, "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.GatherInfo" }, - "hangup": { + "completion_action": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "optional" }, - "hold": { + "has_questions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "timeout", - "type": "int", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "bool" }, - "join_conference": { + "questions": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "list" + }, + "to_json": { + "params": [ { - "name": "name", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "GatherQuestion": { + "methods": { + "__init__": { + "params": [ { - "name": "muted", - "type": "bool", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "beep", + "name": "key", "type": "string", - "required": false, - "default": null - }, - { - "name": "start_on_enter", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "end_on_exit", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "wait_url", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "max_participants", - "type": "int", - "required": false, - "default": null + "required": true }, { - "name": "record", + "name": "question", "type": "string", - "required": false, - "default": null - }, - { - "name": "region", - "type": "optional", - "required": false, - "default": null + "required": true }, { - "name": "trim", + "name": "type", "type": "string", "required": false, - "default": null - }, - { - "name": "coach", - "type": "optional", - "required": false, - "default": null + "default": "string" }, { - "name": "status_callback_event", - "type": "optional", + "name": "confirm", + "type": "bool", "required": false, - "default": null + "default": false }, { - "name": "status_callback", + "name": "prompt", "type": "optional", "required": false, "default": null }, { - "name": "status_callback_method", - "type": "string", + "name": "functions", + "type": "list", "required": false, - "default": null + "default": [] }, { - "name": "recording_status_callback", - "type": "optional", + "name": "isolated", + "type": "optional", "required": false, "default": null - }, + } + ], + "returns": "void" + }, + "confirm": { + "params": [ { - "name": "recording_status_callback_method", - "type": "string", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "functions": { + "params": [ { - "name": "recording_status_callback_event", - "type": "string", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "isolated": { + "params": [ { - "name": "result", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "optional" }, - "join_room": { + "key": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "prompt": { + "params": [ { - "name": "name", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "optional" }, - "pay": { + "question": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "to_json": { + "params": [ { - "name": "payment_connector_url", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "type": { + "params": [ { - "name": "input_method", - "type": "string", - "required": false, - "default": null - }, - { - "name": "status_url", - "type": "string", - "required": false, - "default": null - }, - { - "name": "payment_method", - "type": "string", - "required": false, - "default": null - }, - { - "name": "timeout", - "type": "int", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "string" + } + } + }, + "Step": { + "methods": { + "__init__": { + "params": [ { - "name": "max_attempts", - "type": "int", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "add_bullets": { + "params": [ { - "name": "security_code", - "type": "bool", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "postal_code", + "name": "title", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "min_postal_code_length", - "type": "int", - "required": false, - "default": null - }, + "name": "bullets", + "type": "list", + "required": true + } + ], + "returns": "class:signalwire.core.contexts.Step" + }, + "add_gather_question": { + "params": [ { - "name": "token_type", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "charge_amount", + "name": "key", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "currency", + "name": "question", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "language", + "name": "type", "type": "string", "required": false, - "default": null + "default": "string" }, { - "name": "voice", - "type": "string", + "name": "confirm", + "type": "bool", "required": false, - "default": null + "default": false }, { - "name": "description", - "type": "string", + "name": "prompt", + "type": "optional", "required": false, "default": null }, { - "name": "valid_card_types", - "type": "string", + "name": "functions", + "type": "list", "required": false, "default": null }, { - "name": "parameters", - "type": "list", + "name": "isolated", + "type": "optional", "required": false, "default": null + } + ], + "returns": "class:signalwire.core.contexts.Step" + }, + "add_section": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "prompts", - "type": "list", - "required": false, - "default": null + "name": "title", + "type": "string", + "required": true }, { - "name": "ai_response", + "name": "body", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "play_background_file": { + "clear_sections": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "class:signalwire.core.contexts.Step" + }, + "functions": { + "params": [ { - "name": "filename", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "optional>>" + }, + "gather_info": { + "params": [ { - "name": "wait", - "type": "bool", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "optional" }, - "record_call": { + "name": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "set_end": { + "params": [ { - "name": "control_id", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "stereo", + "name": "end", "type": "bool", "required": true - }, + } + ], + "returns": "class:signalwire.core.contexts.Step" + }, + "set_functions": { + "params": [ { - "name": "format", - "type": "class:signalwire.swaig.record_format.RecordFormat", - "required": true + "name": "self", + "kind": "self" }, { - "name": "direction", - "type": "class:signalwire.swaig.record_direction.RecordDirection", + "name": "functions", + "type": "union>", "required": true - }, - { - "name": "terminators", - "type": "string", - "required": false, - "default": null - }, - { - "name": "beep", - "type": "bool", - "required": false, - "default": null - }, + } + ], + "returns": "class:signalwire.core.contexts.Step" + }, + "set_gather_info": { + "params": [ { - "name": "input_sensitivity", - "type": "float", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "initial_timeout", - "type": "optional", + "name": "output_key", + "type": "optional", "required": false, "default": null }, { - "name": "end_silence_timeout", - "type": "optional", + "name": "completion_action", + "type": "optional", "required": false, "default": null }, { - "name": "max_length", - "type": "optional", + "name": "prompt", + "type": "optional", "required": false, "default": null }, { - "name": "status_url", - "type": "string", + "name": "isolated", + "type": "bool", "required": false, - "default": null + "default": false } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "remove_global_data": { + "set_history": { "params": [ { "name": "self", "kind": "self" }, { - "name": "keys", - "type": "any", + "name": "history", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "remove_metadata": { + "set_reset_consolidate": { "params": [ { "name": "self", "kind": "self" }, { - "name": "keys", - "type": "any", + "name": "c", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "replace_in_history": { + "set_reset_full_reset": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "any", - "required": true + "name": "fr", + "type": "bool", + "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "rpc_ai_message": { + "set_reset_system_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", - "type": "string", - "required": true - }, - { - "name": "message_text", + "name": "sp", "type": "string", "required": true - }, - { - "name": "role", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "rpc_ai_unhold": { + "set_reset_user_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", + "name": "up", "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "rpc_dial": { + "set_skip_to_next_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "to_number", - "type": "string", - "required": true - }, - { - "name": "from_number", - "type": "string", - "required": true - }, - { - "name": "dest_swml", - "type": "string", + "name": "skip", + "type": "bool", "required": true - }, - { - "name": "device_type", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "say": { + "set_skip_user_turn": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "string", + "name": "skip", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "send_sms": { + "set_step_criteria": { "params": [ { "name": "self", "kind": "self" }, { - "name": "to", - "type": "string", - "required": true - }, - { - "name": "from", + "name": "criteria", "type": "string", "required": true - }, - { - "name": "body", - "type": "string", - "required": false, - "default": null - }, - { - "name": "media", - "type": "list", - "required": false, - "default": null - }, - { - "name": "tags", - "type": "list", - "required": false, - "default": null - }, - { - "name": "region", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "set_end_of_speech_timeout": { + "set_text": { "params": [ { "name": "self", "kind": "self" }, { - "name": "milliseconds", - "type": "int", + "name": "text", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "set_metadata": { + "set_valid_contexts": { "params": [ { "name": "self", "kind": "self" }, { - "name": "data", - "type": "any", + "name": "ctxs", + "type": "list", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "set_post_process": { + "set_valid_steps": { "params": [ { "name": "self", "kind": "self" }, { - "name": "pp", - "type": "bool", + "name": "steps", + "type": "list", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.contexts.Step" }, - "set_response": { + "to_json": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "response", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "set_speech_event_timeout": { + "valid_contexts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "milliseconds", - "type": "int", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "optional>" }, - "simulate_user_input": { + "valid_steps": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "text", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "sip_refer": { + "returns": "optional>" + } + } + } + } + }, + "signalwire.core.data_map": { + "classes": { + "DataMap": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "to_uri", + "name": "function_name", "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "stop": { + "body": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "stop_background_file": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "stop_record_call": { + "description": { "params": [ { "name": "self", "kind": "self" }, { - "name": "control_id", + "name": "desc", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "stop_tap": { + "error_keys": { "params": [ { "name": "self", "kind": "self" }, { - "name": "control_id", - "type": "string", - "required": false, - "default": null + "name": "keys", + "type": "list", + "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "switch_context": { + "expression": { "params": [ { "name": "self", "kind": "self" }, { - "name": "system_prompt", + "name": "test_value", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "user_prompt", + "name": "pattern", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "consolidate", - "type": "bool", - "required": false, - "default": null + "name": "output", + "type": "class:signalwire.core.function_result.FunctionResult", + "required": true }, { - "name": "full_reset", - "type": "bool", + "name": "nomatch_output", + "type": "class:signalwire.core.function_result.FunctionResult", "required": false, "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "swml_change_context": { + "fallback_output": { "params": [ { "name": "self", "kind": "self" }, { - "name": "context_name", - "type": "string", + "name": "result", + "type": "class:signalwire.core.function_result.FunctionResult", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "swml_change_step": { + "foreach": { "params": [ { "name": "self", "kind": "self" }, { - "name": "step_name", - "type": "string", + "name": "foreach_config", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "swml_transfer": { + "function_name": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "global_error_keys": { + "params": [ { - "name": "dest", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "ai_response", - "type": "string", + "name": "keys", + "type": "list", "required": true - }, - { - "name": "final", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "swml_user_event": { + "output": { "params": [ { "name": "self", "kind": "self" }, { - "name": "event_data", - "type": "any", + "name": "result", + "type": "class:signalwire.core.function_result.FunctionResult", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "tap": { + "parameter": { "params": [ { "name": "self", "kind": "self" }, { - "name": "uri", + "name": "name", "type": "string", "required": true }, { - "name": "control_id", + "name": "param_type", "type": "string", "required": true }, { - "name": "direction", - "type": "class:signalwire.swaig.tap_direction.TapDirection", - "required": true - }, - { - "name": "codec", - "type": "class:signalwire.swaig.codec.Codec", + "name": "desc", + "type": "string", "required": true }, { - "name": "rtp_ptime", - "type": "int", + "name": "required", + "type": "bool", "required": false, - "default": null + "default": false }, { - "name": "status_url", - "type": "string", + "name": "enum_values", + "type": "list", "required": false, "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "to_json": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" + "returns": "class:signalwire.core.data_map.DataMap" }, - "to_string": { + "params": { "params": [ { "name": "self", "kind": "self" }, { - "name": "indent", - "type": "int", - "required": false, - "default": null + "name": "data", + "type": "any", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.data_map.DataMap" }, - "toggle_functions": { + "purpose": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_toggles", - "type": "any", + "name": "desc", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" }, - "update_global_data": { + "to_swaig_function": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "update_settings": { + "webhook": { "params": [ { "name": "self", "kind": "self" }, { - "name": "settings", - "type": "any", + "name": "method", + "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "wait_for_user": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "url", + "type": "string", + "required": true }, { - "name": "enabled", - "type": "optional", + "name": "headers", + "type": "any", "required": false, "default": null }, { - "name": "timeout", - "type": "optional", + "name": "form_param", + "type": "string", "required": false, "default": null }, { - "name": "answer_first", + "name": "input_args_as_params", "type": "bool", "required": false, + "default": false + }, + { + "name": "require_args", + "type": "list", + "required": false, "default": null } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "class:signalwire.core.data_map.DataMap" + }, + "webhook_expressions": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "expressions", + "type": "list", + "required": true + } + ], + "returns": "class:signalwire.core.data_map.DataMap" } } } } }, - "signalwire.core.logging_config": { - "functions": { - "configure_logging": { - "params": [], - "returns": "void" - }, - "get_execution_mode": { - "params": [], - "returns": "string" - }, - "get_logger": { - "params": [ - { - "name": "name", - "type": "string", - "required": true - } - ], - "returns": "bool" - }, - "reset_logging_configuration": { - "params": [], - "returns": "void" - }, - "strip_control_chars": { - "params": [ - { - "name": "value", - "type": "string", - "required": true - } - ], - "returns": "string" - } - } - }, - "signalwire.core.mixins.ai_config_mixin": { + "signalwire.core.function_result": { "classes": { - "AIConfigMixin": { + "FunctionResult": { "methods": { - "add_function_include": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "include", - "type": "any", - "required": true + "name": "response", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "post_process", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "void" }, - "add_hint": { + "add_action": { "params": [ { "name": "self", "kind": "self" }, { - "name": "hint", + "name": "name", "type": "string", "required": true + }, + { + "name": "data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "add_hints": { + "add_actions": { "params": [ { "name": "self", "kind": "self" }, { - "name": "hints", - "type": "list", + "name": "actions", + "type": "list", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "add_internal_filler": { + "add_dynamic_hints": { "params": [ { "name": "self", "kind": "self" }, { - "name": "lang", - "type": "string", - "required": true - }, - { - "name": "fillers", - "type": "list", + "name": "hints", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "add_language": { + "clear_dynamic_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "lang", - "type": "class:signalwire.agent.language_config.LanguageConfig", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "add_mcp_server": { + "connect": { "params": [ { "name": "self", "kind": "self" }, { - "name": "url", + "name": "destination", "type": "string", "required": true }, { - "name": "headers", - "type": "dict", - "required": false, - "default": null - }, - { - "name": "resources", + "name": "final", "type": "bool", "required": false, - "default": null + "default": true }, { - "name": "resource_vars", - "type": "dict", + "name": "from_addr", + "type": "string", "required": false, "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "add_pattern_hint": { + "create_payment_action": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "hint", + "name": "action_type", "type": "string", "required": true }, { - "name": "pattern", + "name": "phrase", "type": "string", "required": true - }, - { - "name": "replace", + } + ], + "returns": "any" + }, + "create_payment_parameter": { + "params": [ + { + "name": "name", "type": "string", "required": true }, { - "name": "ignore_case", - "type": "bool", - "required": false, - "default": null + "name": "value", + "type": "string", + "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "add_pronunciation": { + "create_payment_prompt": { "params": [ { - "name": "self", - "kind": "self" + "name": "for_situation", + "type": "string", + "required": true }, { - "name": "replace_val", - "type": "string", + "name": "actions", + "type": "list", "required": true }, { - "name": "with_val", + "name": "card_type", "type": "string", - "required": true + "required": false, + "default": null }, { - "name": "ignore_case", - "type": "bool", + "name": "error_type", + "type": "string", "required": false, "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "enable_debug_events": { + "enable_extensive_data": { "params": [ { "name": "self", "kind": "self" }, { - "name": "enable", + "name": "enabled", "type": "bool", "required": false, - "default": null + "default": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "enable_mcp_server": { + "enable_functions_on_timeout": { "params": [ { "name": "self", "kind": "self" }, { - "name": "enable", + "name": "enabled", "type": "bool", "required": false, - "default": null + "default": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "get_language_params": { + "execute_rpc": { "params": [ { "name": "self", "kind": "self" }, { - "name": "code", + "name": "method", "type": "string", "required": true + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + }, + { + "name": "call_id", + "type": "string", + "required": false, + "default": null + }, + { + "name": "node_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "optional" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_function_includes": { + "execute_swml": { "params": [ { "name": "self", "kind": "self" }, { - "name": "includes", - "type": "list", + "name": "swml_content", + "type": "any", "required": true + }, + { + "name": "transfer", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_global_data": { + "hangup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_internal_fillers": { + "hold": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fillers", - "type": "any", - "required": true + "name": "timeout", + "type": "int", + "required": false, + "default": 300 } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_language_params": { + "join_conference": { "params": [ { "name": "self", "kind": "self" }, { - "name": "code", + "name": "name", "type": "string", "required": true }, { - "name": "params", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_languages": { - "params": [ + "name": "muted", + "type": "bool", + "required": false, + "default": false + }, { - "name": "self", - "kind": "self" + "name": "beep", + "type": "string", + "required": false, + "default": "true" }, { - "name": "langs", - "type": "list", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_multilingual": { - "params": [ + "name": "start_on_enter", + "type": "bool", + "required": false, + "default": true + }, { - "name": "self", - "kind": "self" + "name": "end_on_exit", + "type": "bool", + "required": false, + "default": false }, { - "name": "config", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_native_functions": { - "params": [ + "name": "wait_url", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "max_participants", + "type": "int", + "required": false, + "default": 250 }, { - "name": "funcs", - "type": "list", - "required": true - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_param": { - "params": [ + "name": "record", + "type": "string", + "required": false, + "default": "do-not-record" + }, { - "name": "self", - "kind": "self" + "name": "region", + "type": "optional", + "required": false, + "default": null }, { - "name": "key", + "name": "trim", "type": "string", - "required": true + "required": false, + "default": "trim-silence" }, { - "name": "value", - "type": "any", - "required": true + "name": "coach", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_callback_event", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_callback", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_callback_method", + "type": "string", + "required": false, + "default": "POST" + }, + { + "name": "recording_status_callback", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "recording_status_callback_method", + "type": "string", + "required": false, + "default": "POST" + }, + { + "name": "recording_status_callback_event", + "type": "string", + "required": false, + "default": "completed" + }, + { + "name": "result", + "type": "optional", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_params": { + "join_room": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "name", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_post_prompt_llm_params": { + "pay": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_prompt_llm_params": { - "params": [ + "name": "payment_connector_url", + "type": "string", + "required": true + }, { - "name": "self", - "kind": "self" + "name": "input_method", + "type": "string", + "required": false, + "default": "dtmf" }, { - "name": "params", - "type": "any", + "name": "status_url", + "type": "string", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.agent_base.AgentBase" - }, - "set_pronunciations": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "payment_method", + "type": "string", + "required": false, + "default": "credit-card" }, { - "name": "pronuns", - "type": "list", - "required": true + "name": "timeout", + "type": "int", + "required": false, + "default": 5 + }, + { + "name": "max_attempts", + "type": "int", + "required": false, + "default": 1 + }, + { + "name": "security_code", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "postal_code", + "type": "union", + "required": false, + "default": true + }, + { + "name": "min_postal_code_length", + "type": "int", + "required": false, + "default": 0 + }, + { + "name": "token_type", + "type": "string", + "required": false, + "default": "reusable" + }, + { + "name": "charge_amount", + "type": "string", + "required": false, + "default": null + }, + { + "name": "currency", + "type": "string", + "required": false, + "default": "usd" + }, + { + "name": "language", + "type": "string", + "required": false, + "default": "en-US" + }, + { + "name": "voice", + "type": "string", + "required": false, + "default": "woman" + }, + { + "name": "description", + "type": "string", + "required": false, + "default": null + }, + { + "name": "valid_card_types", + "type": "string", + "required": false, + "default": "visa mastercard amex" + }, + { + "name": "parameters", + "type": "list", + "required": false, + "default": null + }, + { + "name": "prompts", + "type": "list", + "required": false, + "default": null + }, + { + "name": "ai_response", + "type": "string", + "required": false, + "default": "The payment status is ${pay_result}, do not mention anything else about collecting payment if successful." } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "update_global_data": { + "play_background_file": { "params": [ { "name": "self", "kind": "self" }, { - "name": "data", - "type": "any", + "name": "filename", + "type": "string", "required": true + }, + { + "name": "wait", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - } - } - } - } - }, - "signalwire.core.mixins.auth_mixin": { - "classes": { - "AuthMixin": { - "methods": { - "get_basic_auth_credentials": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "post_process": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "tuple" + "returns": "bool" }, - "validate_basic_auth": { + "record_call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "username", + "name": "control_id", "type": "string", - "required": true + "required": false, + "default": null }, { - "name": "password", + "name": "stereo", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "format", + "type": "class:signalwire.swaig.record_format.RecordFormat", + "required": false, + "default": "wav" + }, + { + "name": "direction", + "type": "class:signalwire.swaig.record_direction.RecordDirection", + "required": false, + "default": "both" + }, + { + "name": "terminators", "type": "string", - "required": true + "required": false, + "default": null + }, + { + "name": "beep", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "input_sensitivity", + "type": "float", + "required": false, + "default": 44.0 + }, + { + "name": "initial_timeout", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "end_silence_timeout", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "max_length", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "type": "string", + "required": false, + "default": null } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.mixins.prompt_mixin": { - "classes": { - "PromptMixin": { - "methods": { - "contexts": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "remove_global_data": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "keys", + "type": "any", + "required": true } ], - "returns": "optional" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "define_contexts": { + "remove_metadata": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "keys", + "type": "any", + "required": true } ], - "returns": "class:signalwire.core.contexts.ContextBuilder" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "get_post_prompt": { + "replace_in_history": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "text", + "type": "any", + "required": false, + "default": true } ], - "returns": "optional" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "get_prompt": { + "response": { "params": [ { "name": "self", @@ -4856,71 +4860,102 @@ ], "returns": "string" }, - "prompt_add_section": { + "rpc_ai_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "call_id", "type": "string", "required": true }, { - "name": "body", + "name": "message_text", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "bullets", - "type": "list", + "name": "role", + "type": "string", "required": false, - "default": null + "default": "system" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "prompt_add_subsection": { + "rpc_ai_unhold": { "params": [ { "name": "self", "kind": "self" }, { - "name": "parent_title", + "name": "call_id", "type": "string", "required": true + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "rpc_dial": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "title", + "name": "to_number", "type": "string", "required": true }, { - "name": "body", + "name": "from_number", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "bullets", - "type": "list", + "name": "dest_swml", + "type": "string", + "required": true + }, + { + "name": "device_type", + "type": "string", "required": false, - "default": null + "default": "phone" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "prompt_add_to_section": { + "say": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "text", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "send_sms": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "to", + "type": "string", + "required": true + }, + { + "name": "from", "type": "string", "required": true }, @@ -4931,1049 +4966,1189 @@ "default": null }, { - "name": "bullets", + "name": "media", + "type": "list", + "required": false, + "default": null + }, + { + "name": "tags", "type": "list", "required": false, "default": null + }, + { + "name": "region", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "prompt_has_section": { + "set_end_of_speech_timeout": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", - "type": "string", + "name": "milliseconds", + "type": "int", "required": true } ], - "returns": "bool" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "reset_contexts": { + "set_metadata": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_post_prompt": { + "set_post_process": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "string", + "name": "pp", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_prompt_pom": { + "set_response": { "params": [ { "name": "self", "kind": "self" }, { - "name": "pom", - "type": "list", + "name": "response", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "set_prompt_text": { + "set_speech_event_timeout": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "string", + "name": "milliseconds", + "type": "int", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - } - } - } - } - }, - "signalwire.core.mixins.serverless_mixin": { - "classes": { - "ServerlessMixin": { - "methods": { - "handle_serverless_request": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "simulate_user_input": { "params": [ { "name": "self", "kind": "self" }, { - "name": "event", - "type": "any", - "required": false, - "default": null - }, - { - "name": "context", - "type": "any", - "required": false, - "default": null - }, - { - "name": "mode", + "name": "text", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.utils.serverless_response.ServerlessResponse" - } - } - } - } - }, - "signalwire.core.mixins.skill_mixin": { - "classes": { - "SkillMixin": { - "methods": { - "add_skill": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "sip_refer": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", + "name": "to_uri", "type": "string", "required": true - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "has_skill": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "list_skills": { + "stop_background_file": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "remove_skill": { + "stop_record_call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", + "name": "control_id", "type": "string", - "required": true + "required": false, + "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - } - } - } - } - }, - "signalwire.core.mixins.state_mixin": { - "classes": { - "StateMixin": { - "methods": { - "validate_tool_token": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "stop_tap": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_name", + "name": "control_id", "type": "string", - "required": true + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "switch_context": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "token", + "name": "system_prompt", "type": "string", - "required": true + "required": false, + "default": null }, { - "name": "call_id", + "name": "user_prompt", "type": "string", - "required": true + "required": false, + "default": null + }, + { + "name": "consolidate", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "full_reset", + "type": "bool", + "required": false, + "default": false } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.mixins.tool_mixin": { - "classes": { - "ToolMixin": { - "methods": { - "define_tool": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "swml_change_context": { "params": [ { "name": "self", "kind": "self" }, { - "name": "tool", - "type": "class:signalwire.core.swaig_function.ToolDefinition", + "name": "context_name", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "define_tools": { + "swml_change_step": { "params": [ { "name": "self", "kind": "self" }, { - "name": "tools", - "type": "list", + "name": "step_name", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "on_function_call": { + "swml_transfer": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "dest", "type": "string", "required": true }, { - "name": "args", - "type": "any", + "name": "ai_response", + "type": "string", "required": true }, { - "name": "raw_data", + "name": "final", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "swml_user_event": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "event_data", "type": "any", "required": true } ], "returns": "class:signalwire.core.function_result.FunctionResult" }, - "register_swaig_function": { + "tap": { "params": [ { "name": "self", "kind": "self" }, { - "name": "func_def", - "type": "any", + "name": "uri", + "type": "string", "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null + }, + { + "name": "direction", + "type": "class:signalwire.swaig.tap_direction.TapDirection", + "required": false, + "default": "both" + }, + { + "name": "codec", + "type": "class:signalwire.swaig.codec.Codec", + "required": false, + "default": "PCMU" + }, + { + "name": "rtp_ptime", + "type": "int", + "required": false, + "default": 20 + }, + { + "name": "status_url", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.agent_base.AgentBase" - } - } - } - } - }, - "signalwire.core.mixins.web_mixin": { - "classes": { - "WebMixin": { - "methods": { - "as_router": { + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "to_json": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.web.HostAppRouter" + "returns": "any" }, - "enable_debug_routes": { + "to_string": { "params": [ { "name": "self", "kind": "self" }, { - "name": "enable", - "type": "bool", + "name": "indent", + "type": "int", "required": false, - "default": null + "default": -1 } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "string" }, - "manual_set_proxy_url": { + "toggle_functions": { "params": [ { "name": "self", "kind": "self" }, { - "name": "url", - "type": "string", + "name": "function_toggles", + "type": "any", "required": true } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "on_request": { + "update_global_data": { "params": [ { "name": "self", "kind": "self" }, { - "name": "request_data", - "type": "optional", - "required": false, - "default": null + "name": "data", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "update_settings": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "callback_path", - "type": "optional", - "required": false, - "default": null + "name": "settings", + "type": "any", + "required": true } ], - "returns": "optional" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "on_swml_request": { + "wait_for_user": { "params": [ { "name": "self", "kind": "self" }, { - "name": "request_data", - "type": "optional", + "name": "enabled", + "type": "optional", "required": false, "default": null }, { - "name": "callback_path", - "type": "optional", + "name": "timeout", + "type": "optional", "required": false, "default": null + }, + { + "name": "answer_first", + "type": "bool", + "required": false, + "default": false } ], - "returns": "optional" - }, - "register_routing_callback": { + "returns": "class:signalwire.core.function_result.FunctionResult" + } + } + } + } + }, + "signalwire.core.logging_config": { + "functions": { + "configure_logging": { + "params": [], + "returns": "void" + }, + "get_execution_mode": { + "params": [], + "returns": "string" + }, + "get_logger": { + "params": [ + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.logging.logger.Logger" + }, + "reset_logging_configuration": { + "params": [], + "returns": "void" + }, + "strip_control_chars": { + "params": [ + { + "name": "value", + "type": "string", + "required": true + } + ], + "returns": "string" + } + } + }, + "signalwire.core.mixins.ai_config_mixin": { + "classes": { + "AIConfigMixin": { + "methods": { + "add_function_include": { "params": [ { "name": "self", "kind": "self" }, { - "name": "callback", - "type": "class:signalwire.routing_callback.RoutingCallback", + "name": "include", + "type": "any", "required": true - }, - { - "name": "path", - "type": "string", - "required": false, - "default": null } ], "returns": "class:signalwire.core.agent_base.AgentBase" }, - "run": { + "add_hint": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "hint", + "type": "string", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "serve": { + "add_hints": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "hints", + "type": "list", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "set_dynamic_config_callback": { + "add_internal_filler": { "params": [ { "name": "self", "kind": "self" }, { - "name": "cb", - "type": "callable,dict,dict,class:signalwire.core.agent_base.AgentBase>,void>", + "name": "lang", + "type": "string", + "required": true + }, + { + "name": "fillers", + "type": "list", "required": true } ], "returns": "class:signalwire.core.agent_base.AgentBase" }, - "setup_graceful_shutdown": { + "add_language": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - } - } - } - } - }, - "signalwire.core.pom_builder": { - "classes": { - "PomBuilder": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "lang", + "type": "class:signalwire.agent.language_config.LanguageConfig", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_section": { + "add_mcp_server": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "url", "type": "string", "required": true }, { - "name": "body", - "type": "string", + "name": "headers", + "type": "dict", "required": false, "default": null }, { - "name": "bullets", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "numbered", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "numbered_bullets", + "name": "resources", "type": "bool", "required": false, - "default": null + "default": false }, { - "name": "subsections", - "type": "optional>", + "name": "resource_vars", + "type": "dict", "required": false, "default": null } ], - "returns": "class:signalwire.core.pom_builder.PomBuilder" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_subsection": { + "add_pattern_hint": { "params": [ { "name": "self", "kind": "self" }, { - "name": "parent_title", + "name": "hint", "type": "string", "required": true }, { - "name": "title", + "name": "pattern", "type": "string", "required": true }, { - "name": "body", + "name": "replace", "type": "string", - "required": false, - "default": null + "required": true }, { - "name": "bullets", - "type": "optional>", + "name": "ignore_case", + "type": "bool", "required": false, - "default": null + "default": false } ], - "returns": "class:signalwire.core.pom_builder.PomBuilder" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "add_to_section": { + "add_pronunciation": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "replace_val", "type": "string", "required": true }, { - "name": "body", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "bullet", - "type": "optional", - "required": false, - "default": null + "name": "with_val", + "type": "string", + "required": true }, { - "name": "bullets", - "type": "optional>", + "name": "ignore_case", + "type": "bool", "required": false, - "default": null + "default": false } ], - "returns": "class:signalwire.core.pom_builder.PomBuilder" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "from_sections": { + "enable_debug_events": { "params": [ { - "name": "sections", - "type": "any", - "required": true + "name": "self", + "kind": "self" + }, + { + "name": "level", + "type": "int", + "required": false, + "default": 1 } ], - "returns": "class:signalwire.core.pom_builder.PomBuilder" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_section": { + "enable_mcp_server": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", - "type": "string", - "required": true + "name": "enable", + "type": "bool", + "required": false, + "default": true } ], - "returns": "class:signalwire.pom.pom.Section" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "has_section": { + "get_language_params": { "params": [ { "name": "self", "kind": "self" }, { - "name": "title", + "name": "code", "type": "string", "required": true } ], - "returns": "bool" + "returns": "optional" }, - "pom": { + "set_function_includes": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "includes", + "type": "list", + "required": true } ], - "returns": "class:signalwire.pom.pom.PromptObjectModel" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "render_markdown": { + "set_global_data": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "data", + "type": "any", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "render_xml": { + "set_internal_fillers": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "fillers", + "type": "any", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "to_dict": { + "set_language_params": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "code", + "type": "string", + "required": true + }, + { + "name": "params", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "to_json": { + "set_languages": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "langs", + "type": "list", + "required": true } ], - "returns": "string" - } - } - } - } - }, - "signalwire.core.post_prompt_generated": { - "classes": { - "PostPrompt": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_multilingual": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "config", + "type": "any", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "call_log": { + "set_native_functions": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "funcs", + "type": "list", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "post_prompt_data": { + "set_param": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "key", + "type": "string", + "required": true + }, + { + "name": "value", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "raw_call_log": { + "set_params": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "swaig_log": { + "set_post_prompt_llm_params": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "times": { + "set_prompt_llm_params": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" - } - } - }, - "PostPromptAssistantEntry": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "set_pronunciations": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "pronuns", + "type": "list", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "stamps_us": { + "update_global_data": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "data", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" } } - }, - "PostPromptSwaigLogEntry": { + } + } + }, + "signalwire.core.mixins.auth_mixin": { + "classes": { + "AuthMixin": { "methods": { - "__init__": { + "get_basic_auth_credentials": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "tuple" }, - "post_data": { + "validate_basic_auth": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - } - } - }, - "PostPromptUserEntry": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "username", + "type": "string", + "required": true + }, + { + "name": "password", + "type": "string", + "required": true } ], - "returns": "void" - }, - "entity": { + "returns": "bool" + } + } + } + } + }, + "signalwire.core.mixins.prompt_mixin": { + "classes": { + "PromptMixin": { + "methods": { + "contexts": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "optional" }, - "eot": { + "define_contexts": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "class:signalwire.core.contexts.ContextBuilder" }, - "timing": { + "get_post_prompt": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" - } - } - } - } - }, - "signalwire.core.security.security_utils": { - "functions": { - "filter_sensitive_headers": { - "params": [ - { - "name": "headers", - "type": "dict", - "required": true - } - ], - "returns": "dict" - }, - "is_valid_hostname": { - "params": [ - { - "name": "host", - "type": "string", - "required": true - } - ], - "returns": "bool" - }, - "redact_url": { - "params": [ - { - "name": "url", - "type": "string", - "required": true - } - ], - "returns": "string" - } - } - }, - "signalwire.core.security.session_manager": { - "classes": { - "SessionManager": { - "methods": { - "__init__": { + "returns": "optional" + }, + "get_prompt": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "activate_session": { + "prompt_add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", + "name": "title", "type": "string", "required": true - } - ], - "returns": "bool" - }, - "create_session": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "call_id", + "name": "body", "type": "string", "required": false, - "default": null + "default": "" + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": [] } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "create_token": { + "prompt_add_subsection": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_name", + "name": "parent_title", "type": "string", "required": true }, { - "name": "call_id", + "name": "title", "type": "string", "required": true }, { - "name": "expiry_seconds", - "type": "int", + "name": "body", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "optional>", "required": false, "default": null } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "create_tool_token": { + "prompt_add_to_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_name", + "name": "title", "type": "string", "required": true }, { - "name": "call_id", + "name": "body", "type": "string", - "required": true + "required": false, + "default": null + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": [] } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "debug_token": { + "prompt_has_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "token", + "name": "title", "type": "string", "required": true } ], - "returns": "any" + "returns": "bool" }, - "end_session": { + "reset_contexts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "generate_token": { + "set_post_prompt": { "params": [ { "name": "self", "kind": "self" }, { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "call_id", + "name": "text", "type": "string", "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_session_metadata": { + "set_prompt_pom": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", - "type": "string", + "name": "pom", + "type": "list", "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "set_debug_mode": { + "set_prompt_text": { "params": [ { "name": "self", "kind": "self" }, { - "name": "enabled", - "type": "bool", + "name": "text", + "type": "string", "required": true } ], - "returns": "void" - }, - "set_session_metadata": { + "returns": "class:signalwire.core.agent_base.AgentBase" + } + } + } + } + }, + "signalwire.core.mixins.serverless_mixin": { + "classes": { + "ServerlessMixin": { + "methods": { + "handle_serverless_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", + "name": "event", + "type": "any", + "required": false, + "default": null + }, + { + "name": "context", + "type": "any", + "required": false, + "default": null + }, + { + "name": "mode", "type": "string", - "required": true + "required": false, + "default": null + } + ], + "returns": "class:signalwire.utils.serverless_response.ServerlessResponse" + } + } + } + } + }, + "signalwire.core.mixins.skill_mixin": { + "classes": { + "SkillMixin": { + "methods": { + "add_skill": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "key", + "name": "skill_name", "type": "string", "required": true }, { - "name": "value", + "name": "params", "type": "any", - "required": true + "required": false, + "default": null } ], - "returns": "bool" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "timing_safe_compare": { + "has_skill": { "params": [ { - "name": "a", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "b", + "name": "skill_name", "type": "string", "required": true } ], "returns": "bool" }, - "validate_token": { + "list_skills": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "token", - "type": "string", - "required": true - }, + } + ], + "returns": "list" + }, + "remove_skill": { + "params": [ { - "name": "function_name", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "call_id", + "name": "skill_name", "type": "string", "required": true } ], - "returns": "bool" - }, + "returns": "class:signalwire.core.agent_base.AgentBase" + } + } + } + } + }, + "signalwire.core.mixins.state_mixin": { + "classes": { + "StateMixin": { + "methods": { "validate_tool_token": { "params": [ { @@ -6002,226 +6177,235 @@ } } }, - "signalwire.core.security.webhook_middleware": { - "functions": { - "validate": { - "params": [ - { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "headers", - "type": "dict", - "required": true - }, - { - "name": "body", - "type": "string", - "required": true - }, - { - "name": "signing_key", - "type": "string", - "required": true, - "kind": "keyword" - } - ], - "returns": "optional,string>>" - } - } - }, - "signalwire.core.security.webhook_validator": { - "functions": { - "validate_request": { - "params": [ - { - "name": "signing_key", - "type": "string", - "required": true - }, - { - "name": "signature", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "params_or_raw_body", - "type": "union>>>", - "required": true - } - ], - "returns": "bool" - }, - "validate_webhook_signature": { - "params": [ - { - "name": "signing_key", - "type": "string", - "required": true - }, - { - "name": "signature", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "raw_body", - "type": "string", - "required": true - } - ], - "returns": "bool" - } - } - }, - "signalwire.core.security_config": { + "signalwire.core.mixins.tool_mixin": { "classes": { - "SecurityConfig": { + "ToolMixin": { "methods": { - "__init__": { + "define_tool": { "params": [ { "name": "self", "kind": "self" }, { - "name": "config_file", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "service_name", - "type": "optional", - "required": false, - "default": null + "name": "tool", + "type": "class:signalwire.core.swaig_function.ToolDefinition", + "required": true } ], - "returns": "void" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "allowed_hosts": { + "define_tools": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "tools", + "type": "list", + "required": true } ], - "returns": "list" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "basic_auth_password": { + "on_function_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "any", + "required": false, + "default": null } ], - "returns": "optional" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "basic_auth_user": { + "register_swaig_function": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "func_def", + "type": "any", + "required": true } ], - "returns": "optional" - }, - "cors_origins": { + "returns": "class:signalwire.core.agent_base.AgentBase" + } + } + } + } + }, + "signalwire.core.mixins.web_mixin": { + "classes": { + "WebMixin": { + "methods": { + "as_router": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "class:signalwire.core.web.HostAppRouter" }, - "domain": { + "enable_debug_routes": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "enable", + "type": "bool", + "required": false, + "default": true } ], - "returns": "optional" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_basic_auth": { + "manual_set_proxy_url": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "url", + "type": "string", + "required": true } ], - "returns": "tuple" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_cors_config": { + "on_request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", + "required": false, + "default": null } ], - "returns": "any" + "returns": "optional" }, - "get_security_headers": { + "on_swml_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "is_https", - "type": "bool", + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", "required": false, "default": null } ], - "returns": "any" + "returns": "optional" }, - "get_ssl_context_kwargs": { + "register_routing_callback": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "callback", + "type": "class:signalwire.routing_callback.RoutingCallback", + "required": true + }, + { + "name": "path", + "type": "string", + "required": false, + "default": "/sip" } ], - "returns": "any" + "returns": "class:signalwire.core.agent_base.AgentBase" }, - "get_url_scheme": { + "run": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "hsts_max_age": { + "serve": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "void" }, - "load_from_env": { + "set_dynamic_config_callback": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "cb", + "type": "callable,dict,dict,class:signalwire.core.agent_base.AgentBase>,void>", + "required": true + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "setup_graceful_shutdown": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + } + } + } + } + }, + "signalwire.core.pom_builder": { + "classes": { + "PomBuilder": { + "methods": { + "__init__": { "params": [ { "name": "self", @@ -6230,89 +6414,170 @@ ], "returns": "void" }, - "log_config": { + "add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "service_name", + "name": "title", "type": "string", "required": true + }, + { + "name": "body", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "numbered", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "numbered_bullets", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "subsections", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.core.pom_builder.PomBuilder" }, - "max_request_size": { + "add_subsection": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "parent_title", + "type": "string", + "required": true + }, + { + "name": "title", + "type": "string", + "required": true + }, + { + "name": "body", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "int" + "returns": "class:signalwire.core.pom_builder.PomBuilder" }, - "rate_limit": { + "add_to_section": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "title", + "type": "string", + "required": true + }, + { + "name": "body", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "bullet", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "bullets", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "int" + "returns": "class:signalwire.core.pom_builder.PomBuilder" }, - "request_timeout": { + "from_sections": { "params": [ { - "name": "self", - "kind": "self" + "name": "sections", + "type": "any", + "required": true } ], - "returns": "int" + "returns": "class:signalwire.core.pom_builder.PomBuilder" }, - "should_allow_host": { + "get_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "host", + "name": "title", "type": "string", "required": true } ], - "returns": "bool" + "returns": "class:signalwire.pom.pom.Section" }, - "ssl_cert_path": { + "has_section": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "title", + "type": "string", + "required": true } ], - "returns": "optional" + "returns": "bool" }, - "ssl_enabled": { + "pom": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "class:signalwire.pom.pom.PromptObjectModel" }, - "ssl_key_path": { + "render_markdown": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "string" }, - "ssl_verify_mode": { + "render_xml": { "params": [ { "name": "self", @@ -6321,31 +6586,31 @@ ], "returns": "string" }, - "use_hsts": { + "to_dict": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "validate_ssl_config": { + "to_json": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.ssl_validation_result.SslValidationResult" + "returns": "string" } } } } }, - "signalwire.core.skill_base": { + "signalwire.core.post_prompt_generated": { "classes": { - "SkillBase": { + "PostPrompt": { "methods": { "__init__": { "params": [ @@ -6356,60 +6621,34 @@ ], "returns": "void" }, - "cleanup": { + "call_log": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "define_tool": { + "post_prompt_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "description", - "type": "string", - "required": true - }, - { - "name": "parameters", - "type": "any", - "required": true - }, - { - "name": "handler", - "type": "callable,class:signalwire.core.function_result.FunctionResult>", - "required": true - }, - { - "name": "secure", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swaig_function.ToolDefinition" + "returns": "any" }, - "get_datamap_functions": { + "raw_call_log": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "get_global_data": { + "swaig_log": { "params": [ { "name": "self", @@ -6418,587 +6657,703 @@ ], "returns": "any" }, - "get_hints": { + "times": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" - }, - "get_instance_key": { + "returns": "any" + } + } + }, + "PostPromptAssistantEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "get_param_or_env": { + "stamps_us": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "env_var", - "type": "string", - "required": true - }, - { - "name": "default_val", - "type": "string", - "required": false, - "default": null } ], - "returns": "string" - }, - "get_parameter_schema": { + "returns": "any" + } + } + }, + "PostPromptSwaigLogEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "get_prompt_sections": { + "post_data": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" - }, - "get_skill_data": { + "returns": "any" + } + } + }, + "PostPromptUserEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "any" + "returns": "void" }, - "register_tools": { + "entity": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "required_env_vars": { + "eot": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "required_packages": { + "timing": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" - }, - "setup": { + "returns": "any" + } + } + } + } + }, + "signalwire.core.security.security_utils": { + "functions": { + "filter_sensitive_headers": { + "params": [ + { + "name": "headers", + "type": "dict", + "required": true + } + ], + "returns": "dict" + }, + "is_valid_hostname": { + "params": [ + { + "name": "host", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "redact_url": { + "params": [ + { + "name": "url", + "type": "string", + "required": true + } + ], + "returns": "string" + } + } + }, + "signalwire.core.security.session_manager": { + "classes": { + "SessionManager": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": true + "name": "token_expiry_secs", + "type": "int", + "required": false, + "default": 900 + }, + { + "name": "secret_key", + "type": "string", + "required": false, + "default": "" } ], - "returns": "bool" + "returns": "void" }, - "skill_description": { + "activate_session": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true } ], - "returns": "string" + "returns": "bool" }, - "skill_name": { + "create_session": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": false, + "default": null } ], "returns": "string" }, - "skill_version": { + "create_token": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + }, + { + "name": "expiry_seconds", + "type": "int", + "required": false, + "default": 3600 } ], "returns": "string" }, - "supports_multiple_instances": { + "create_tool_token": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true } ], - "returns": "bool" + "returns": "string" }, - "update_skill_data": { + "debug_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "result", - "type": "class:signalwire.core.function_result.FunctionResult", - "required": true - }, - { - "name": "data", - "type": "any", + "name": "token", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "validate_env_vars": { + "end_session": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true } ], "returns": "bool" }, - "validate_packages": { + "generate_token": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.skill_manager": { - "classes": { - "SkillManager": { - "methods": { - "__init__": { + "returns": "string" + }, + "get_session_metadata": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true } ], - "returns": "void" + "returns": "any" }, - "cleanup_all": { + "secret_key": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "get_skill": { + "set_debug_mode": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", - "type": "string", + "name": "enabled", + "type": "bool", "required": true } ], - "returns": "class:signalwire.core.skill_base.SkillBase" + "returns": "void" }, - "has_skill": { + "set_session_metadata": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", + "name": "call_id", + "type": "string", + "required": true + }, + { + "name": "key", "type": "string", "required": true + }, + { + "name": "value", + "type": "any", + "required": true } ], "returns": "bool" }, - "is_loaded": { + "timing_safe_compare": { "params": [ { - "name": "self", - "kind": "self" + "name": "a", + "type": "string", + "required": true }, { - "name": "skill_name", + "name": "b", "type": "string", "required": true } ], "returns": "bool" }, - "list_loaded": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "list" - }, - "list_loaded_skills": { + "token_expiry_secs": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "int" }, - "load_skill": { + "validate_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", + "name": "token", "type": "string", "required": true }, { - "name": "params", - "type": "any", + "name": "function_name", + "type": "string", "required": true }, { - "name": "agent", - "type": "class:signalwire.core.agent_base.AgentBase", + "name": "call_id", + "type": "string", "required": true } ], "returns": "bool" }, - "unload_skill": { + "validate_tool_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "skill_name", + "name": "function_name", "type": "string", "required": true - } - ], - "returns": "void" - } - } - } - } - }, - "signalwire.core.swaig_function": { - "classes": { - "SWAIGFunction": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "name", + "name": "token", "type": "string", "required": true }, { - "name": "handler", - "type": "class:signalwire.swaig_function_handler.SwaigFunctionHandler", - "required": true - }, - { - "name": "description", + "name": "call_id", "type": "string", "required": true - }, - { - "name": "parameters", - "type": "any", - "required": false, - "default": null - }, - { - "name": "secure", - "type": "bool", - "required": false, - "default": null - }, + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.core.security.webhook_middleware": { + "functions": { + "validate": { + "params": [ + { + "name": "method", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true + }, + { + "name": "body", + "type": "string", + "required": true + }, + { + "name": "signing_key", + "type": "string", + "required": true, + "kind": "keyword" + } + ], + "returns": "optional,string>>" + } + } + }, + "signalwire.core.security.webhook_validator": { + "functions": { + "validate_request": { + "params": [ + { + "name": "signing_key", + "type": "string", + "required": true + }, + { + "name": "signature", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "params_or_raw_body", + "type": "union>>>", + "required": true + } + ], + "returns": "bool" + }, + "validate_webhook_signature": { + "params": [ + { + "name": "signing_key", + "type": "string", + "required": true + }, + { + "name": "signature", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "raw_body", + "type": "string", + "required": true + } + ], + "returns": "bool" + } + } + }, + "signalwire.core.security_config": { + "classes": { + "SecurityConfig": { + "methods": { + "__init__": { + "params": [ { - "name": "fillers", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "wait_file", + "name": "config_file", "type": "optional", "required": false, "default": null }, { - "name": "wait_file_loops", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "webhook_url", + "name": "service_name", "type": "optional", "required": false, "default": null - }, - { - "name": "required", - "type": "list", - "required": false, - "default": null - }, - { - "name": "is_typed_handler", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "extra_swaig_fields", - "type": "any", - "required": false, - "default": null } ], "returns": "void" }, - "call": { + "allowed_hosts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": false, - "default": null } ], - "returns": "any" + "returns": "list" }, - "description": { + "basic_auth_password": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "optional" }, - "execute": { + "basic_auth_user": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "optional", - "required": false, - "default": null } ], - "returns": "any" + "returns": "optional" }, - "extra_swaig_fields": { + "cors_origins": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "list" }, - "fillers": { + "domain": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "optional" }, - "handler": { + "get_basic_auth": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.swaig_function_handler.SwaigFunctionHandler" + "returns": "tuple" }, - "is_external": { + "get_cors_config": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "is_typed_handler": { + "get_security_headers": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "is_https", + "type": "bool", + "required": false, + "default": false } ], - "returns": "bool" + "returns": "any" }, - "name": { + "get_ssl_context_kwargs": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "parameters": { + "get_url_scheme": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "required": { + "hsts_max_age": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "int" }, - "secure": { + "load_from_env": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "void" }, - "to_swaig": { + "log_config": { "params": [ { "name": "self", "kind": "self" }, { - "name": "base_url", + "name": "service_name", "type": "string", "required": true - }, + } + ], + "returns": "void" + }, + "max_request_size": { + "params": [ { - "name": "token", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "rate_limit": { + "params": [ { - "name": "call_id", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "request_timeout": { + "params": [ { - "name": "include_auth", - "type": "bool", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "int" }, - "validate_args": { + "should_allow_host": { "params": [ { "name": "self", "kind": "self" }, { - "name": "args", - "type": "any", + "name": "host", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.args_validation_result.ArgsValidationResult" + "returns": "bool" }, - "wait_file": { + "ssl_cert_path": { "params": [ { "name": "self", @@ -7007,16 +7362,16 @@ ], "returns": "optional" }, - "wait_file_loops": { + "ssl_enabled": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "bool" }, - "webhook_url": { + "ssl_key_path": { "params": [ { "name": "self", @@ -7024,42 +7379,41 @@ } ], "returns": "optional" - } - } - }, - "ToolDefinition": { - "methods": { - "__init__": { + }, + "ssl_verify_mode": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "to_swaig_json": { + "use_hsts": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "bool" + }, + "validate_ssl_config": { + "params": [ { - "name": "web_hook_url", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "class:signalwire.core.ssl_validation_result.SslValidationResult" } } } } }, - "signalwire.core.swaig_request_generated": { + "signalwire.core.skill_base": { "classes": { - "SwaigRequest": { + "SkillBase": { "methods": { "__init__": { "params": [ @@ -7070,118 +7424,69 @@ ], "returns": "void" }, - "argument": { + "agent": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" - } - } - } - } - }, - "signalwire.core.swml_builder": { - "classes": { - "SWMLBuilder": { - "methods": { - "__init__": { + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "cleanup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", - "required": true } ], "returns": "void" }, - "add_section": { + "define_tool": { "params": [ { "name": "self", "kind": "self" }, { - "name": "section_name", + "name": "name", "type": "string", "required": true - } - ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" - }, - "ai": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "prompt_text", - "type": "optional", - "required": false, - "default": null }, { - "name": "prompt_pom", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "post_prompt", - "type": "optional", - "required": false, - "default": null + "name": "description", + "type": "string", + "required": true }, { - "name": "post_prompt_url", - "type": "optional", - "required": false, - "default": null + "name": "parameters", + "type": "any", + "required": true }, { - "name": "swaig", - "type": "optional", - "required": false, - "default": null + "name": "handler", + "type": "callable,class:signalwire.core.function_result.FunctionResult>", + "required": true }, { - "name": "kwargs", - "type": "any", + "name": "secure", + "type": "bool", "required": false, - "default": null + "default": true } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "class:signalwire.core.swaig_function.ToolDefinition" }, - "answer": { + "get_datamap_functions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "max_duration", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "codecs", - "type": "optional", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "list" }, - "build": { + "get_global_data": { "params": [ { "name": "self", @@ -7190,170 +7495,137 @@ ], "returns": "any" }, - "hangup": { + "get_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "reason", - "type": "optional", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "list" }, - "play": { + "get_instance_key": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "url", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "urls", - "type": "optional>", - "required": false, - "default": null - }, + } + ], + "returns": "string" + }, + "get_param_or_env": { + "params": [ { - "name": "volume", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "say_voice", - "type": "optional", - "required": false, - "default": null + "name": "params", + "type": "any", + "required": true }, { - "name": "say_language", - "type": "optional", - "required": false, - "default": null + "name": "key", + "type": "string", + "required": true }, { - "name": "say_gender", - "type": "optional", - "required": false, - "default": null + "name": "env_var", + "type": "string", + "required": true }, { - "name": "auto_answer", - "type": "optional", + "name": "default_val", + "type": "string", "required": false, - "default": null + "default": "" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "string" }, - "render": { + "get_parameter_schema": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "reset": { + "get_prompt_sections": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "list" }, - "say": { + "get_skill_data": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "string", + "name": "raw_data", + "type": "any", "required": true - }, - { - "name": "voice", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "language", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "any" + }, + "params": { + "params": [ { - "name": "gender", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ { - "name": "volume", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "list" }, - "service": { + "required_env_vars": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - } - } - } - } - }, - "signalwire.core.swml_handler": { - "classes": { - "AIVerbHandler": { - "methods": { - "__init__": { + "returns": "list" + }, + "required_packages": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "list" }, - "build_config": { + "setup": { "params": [ { "name": "self", "kind": "self" }, { - "name": "kwargs", + "name": "params", "type": "any", - "required": false, - "default": null + "required": true } ], - "returns": "any" + "returns": "bool" }, - "get_verb_name": { + "skill_description": { "params": [ { "name": "self", @@ -7362,74 +7634,77 @@ ], "returns": "string" }, - "validate_config": { + "skill_name": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "skill_version": { + "params": [ { - "name": "config", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" - } - } - }, - "SWMLVerbHandler": { - "methods": { - "__init__": { + "returns": "string" + }, + "supports_multiple_instances": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "bool" }, - "build_config": { + "update_skill_data": { "params": [ { "name": "self", "kind": "self" }, { - "name": "kwargs", + "name": "result", + "type": "class:signalwire.core.function_result.FunctionResult", + "required": true + }, + { + "name": "data", "type": "any", - "required": false, - "default": null + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "get_verb_name": { + "validate_env_vars": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "bool" }, - "validate_config": { + "validate_packages": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "config", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + "returns": "bool" } } - }, - "VerbHandlerRegistry": { + } + } + }, + "signalwire.core.skill_manager": { + "classes": { + "SkillManager": { "methods": { "__init__": { "params": [ @@ -7440,1026 +7715,918 @@ ], "returns": "void" }, - "get_handler": { + "agent": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "cleanup_all": { + "params": [ { - "name": "verb_name", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.swml_handler.SWMLVerbHandler" + "returns": "void" }, - "get_verb_names": { + "get_skill": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true } ], - "returns": "list" + "returns": "class:signalwire.core.skill_base.SkillBase" }, - "has_handler": { + "has_skill": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", + "name": "skill_name", "type": "string", "required": true } ], "returns": "bool" }, - "register_handler": { + "is_loaded": { "params": [ { "name": "self", "kind": "self" }, { - "name": "handler", - "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", + "name": "skill_name", + "type": "string", "required": true } ], - "returns": "void" - } - } - } - } - }, - "signalwire.core.swml_renderer": { - "classes": { - "SwmlRenderer": { - "methods": { - "__init__": { + "returns": "bool" + }, + "list_loaded": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "list" }, - "render_function_response_swml": { + "list_loaded_skills": { "params": [ { - "name": "response_text", - "type": "string", - "required": true + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "load_skill": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", + "name": "skill_name", + "type": "string", "required": true }, { - "name": "actions", - "type": "optional>", + "name": "skill_class", + "type": "optional", "required": false, "default": null }, { - "name": "format", - "type": "string", + "name": "params", + "type": "optional", "required": false, "default": null } ], - "returns": "string" + "returns": "bool" }, - "render_swml": { + "unload_skill": { "params": [ { - "name": "prompt", - "type": "any", - "required": true + "name": "self", + "kind": "self" }, { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", + "name": "skill_name", + "type": "string", "required": true - }, - { - "name": "opts", - "type": "class:signalwire.core.render_options.RenderOptions", - "required": false, - "default": null } ], - "returns": "string" + "returns": "void" } } } } }, - "signalwire.core.swml_service": { + "signalwire.core.swaig_function": { "classes": { - "SWMLService": { + "SWAIGFunction": { "methods": { "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" - }, - "add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "section_name", + "name": "name", "type": "string", "required": true - } - ], - "returns": "bool" - }, - "add_verb": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "section", - "type": "string", + "name": "handler", + "type": "class:signalwire.swaig_function_handler.SwaigFunctionHandler", "required": true }, { - "name": "verb_name", + "name": "description", "type": "string", "required": true }, { - "name": "params", + "name": "parameters", "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "add_verb_to_section": { - "params": [ + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "secure", + "type": "bool", + "required": false, + "default": false }, { - "name": "section_name", - "type": "string", - "required": true + "name": "fillers", + "type": "optional", + "required": false, + "default": null }, { - "name": "verb_name", - "type": "string", - "required": true + "name": "wait_file", + "type": "optional", + "required": false, + "default": null }, { - "name": "config", + "name": "wait_file_loops", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "webhook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "required", + "type": "list", + "required": false, + "default": [] + }, + { + "name": "is_typed_handler", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "extra_swaig_fields", "type": "any", - "required": true + "required": false, + "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "ai": { + "call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", "type": "any", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "amazon_bedrock": { + "description": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "string" }, - "answer": { + "execute": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "args", "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "optional", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "as_router": { + "extra_swaig_fields": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.web.HostAppRouter" + "returns": "any" }, - "auth_password": { + "fillers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "optional" }, - "auth_username": { + "handler": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "class:signalwire.swaig_function_handler.SwaigFunctionHandler" }, - "build_tool_registry_json": { + "is_external": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "bool" }, - "cond": { + "is_typed_handler": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "bool" }, - "connect": { + "name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "string" }, - "define_tool": { + "parameters": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "tool", - "type": "class:signalwire.core.swaig_function.ToolDefinition", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "denoise": { + "required": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "list" }, - "detect_machine": { + "secure": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "bool" }, - "document": { + "to_swaig": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "class:signalwire.swml.document.Document" - }, - "enter_queue": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "base_url", + "type": "string", + "required": true }, { - "name": "params", - "type": "any", + "name": "token", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "call_id", + "type": "optional", "required": false, "default": null + }, + { + "name": "include_auth", + "type": "bool", + "required": false, + "default": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "execute": { + "validate_args": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "args", "type": "any", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "extract_introspect_payload": { - "params": [ - { - "name": "stdout_capture", - "type": "string", "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.args_validation_result.ArgsValidationResult" }, - "extract_sip_username": { + "wait_file": { "params": [ { - "name": "request_body", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "string" + "returns": "optional" }, - "full_validation_enabled": { + "wait_file_loops": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "optional" }, - "get_all_functions": { + "webhook_url": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "dict" - }, - "get_basic_auth_credentials": { + "returns": "optional" + } + } + }, + "ToolDefinition": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "tuple" + "returns": "void" }, - "get_basic_auth_credentials_with_source": { + "to_swaig_json": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "web_hook_url", + "type": "string", + "required": false, + "default": "" } ], - "returns": "tuple" - }, - "get_document": { + "returns": "any" + } + } + } + } + }, + "signalwire.core.swaig_request_generated": { + "classes": { + "SwaigRequest": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "get_function": { + "argument": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.swaig_function.ToolDefinition" - }, - "get_routing_callback_paths": { + "returns": "any" + } + } + } + } + }, + "signalwire.core.swml_builder": { + "classes": { + "SWMLBuilder": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true } ], - "returns": "list" + "returns": "void" }, - "goto_section": { + "add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null + "name": "section_name", + "type": "string", + "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "handle_request": { + "ai": { "params": [ { "name": "self", "kind": "self" }, { - "name": "method", - "type": "string", - "required": true + "name": "prompt_text", + "type": "optional", + "required": false, + "default": null }, { - "name": "url", - "type": "string", - "required": true + "name": "prompt_pom", + "type": "optional", + "required": false, + "default": null }, { - "name": "headers", - "type": "dict", - "required": true + "name": "post_prompt", + "type": "optional", + "required": false, + "default": null }, { - "name": "body", - "type": "optional", + "name": "post_prompt_url", + "type": "optional", "required": false, "default": null - } - ], - "returns": "tuple,string>" - }, - "hangup": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "swaig", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", + "name": "kwargs", "type": "any", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "has_function": { + "answer": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "max_duration", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "codecs", + "type": "optional", + "required": false, + "default": null } ], - "returns": "bool" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "has_tool": { + "build": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "join_conference": { + "hangup": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "reason", + "type": "optional", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "join_room": { + "play": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "url", + "type": "optional", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "label": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "params", - "type": "any", + "name": "urls", + "type": "optional>", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "list_tool_names": { - "params": [ + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "list" - }, - "live_transcribe": { - "params": [ + "name": "volume", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "say_voice", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", - "type": "any", + "name": "say_language", + "type": "optional", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "live_translate": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "say_gender", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", - "type": "any", + "name": "auto_answer", + "type": "optional", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "manual_set_proxy_url": { + "render": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "proxy_url", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "string" }, - "name": { + "reset": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "on_function_call": { + "say": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "text", "type": "string", "required": true }, { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "on_request": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "request_data", - "type": "optional", + "name": "voice", + "type": "optional", "required": false, "default": null }, { - "name": "callback_path", + "name": "language", "type": "optional", "required": false, "default": null - } - ], - "returns": "optional" - }, - "on_swml_request": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "request_data", - "type": "optional", + "name": "gender", + "type": "optional", "required": false, "default": null }, { - "name": "callback_path", - "type": "optional", + "name": "volume", + "type": "optional", "required": false, "default": null } ], - "returns": "optional" + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" }, - "pay": { + "service": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "play": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "port": { + } + } + } + } + }, + "signalwire.core.swml_handler": { + "classes": { + "AIVerbHandler": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "void" }, - "prompt": { + "build_config": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "prompt_text", + "type": "optional", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "receive_fax": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "params", - "type": "any", + "name": "prompt_pom", + "type": "optional>>", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "record": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "params", - "type": "any", + "name": "contexts", + "type": "optional>", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "record_call": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "params", - "type": "any", + "name": "post_prompt", + "type": "optional", "required": false, "default": null - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "register_routing_callback": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "callback", - "type": "class:signalwire.routing_callback.RoutingCallback", - "required": true - }, - { - "name": "path", - "type": "string", + "name": "post_prompt_url", + "type": "optional", "required": false, "default": null - } - ], - "returns": "void" - }, - "register_swaig_function": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "func_def", - "type": "any", - "required": true + "name": "swaig", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "register_verb_handler": { + "get_verb_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "handler", - "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", - "required": true } ], - "returns": "void" + "returns": "string" }, - "remove_function": { + "validate_config": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "config", + "type": "any", "required": true } ], - "returns": "bool" - }, - "render_document": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, - "render_swml": { + "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + } + } + }, + "SWMLVerbHandler": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "request": { + "build_config": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "kwargs", "type": "any", "required": false, "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "reset_document": { + "get_verb_name": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "return_section": { + "validate_config": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "config", "type": "any", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "route": { + "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + } + } + }, + "VerbHandlerRegistry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "schema_utils": { + "get_handler": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true } ], - "returns": "class:signalwire.utils.schema_utils.SchemaUtils" + "returns": "class:signalwire.core.swml_handler.SWMLVerbHandler" }, - "send_digits": { + "get_verb_names": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "list" }, - "send_fax": { + "has_handler": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null + "name": "verb_name", + "type": "string", + "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "bool" }, - "send_sms": { + "register_handler": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null + "name": "handler", + "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", + "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "serve": { + "returns": "void" + } + } + } + } + }, + "signalwire.core.swml_renderer": { + "classes": { + "SwmlRenderer": { + "methods": { + "__init__": { "params": [ { "name": "self", @@ -8468,97 +8635,249 @@ ], "returns": "void" }, - "set": { + "render_function_response_swml": { "params": [ { - "name": "self", - "kind": "self" + "name": "response_text", + "type": "string", + "required": true }, { - "name": "params", - "type": "any", + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true + }, + { + "name": "actions", + "type": "optional>", "required": false, "default": null + }, + { + "name": "format", + "type": "string", + "required": false, + "default": "json" } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "string" }, - "set_auth": { + "render_swml": { "params": [ { - "name": "self", - "kind": "self" + "name": "prompt", + "type": "any", + "required": true }, { - "name": "username", - "type": "string", + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", "required": true }, { - "name": "password", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "set_host": { + "name": "post_prompt", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "swaig_functions", + "type": "optional>>", + "required": false, + "default": null + }, + { + "name": "startup_hook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "hangup_hook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt_is_pom", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "params", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "add_answer", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "record_call", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "record_format", + "type": "string", + "required": false, + "default": "mp4" + }, + { + "name": "record_stereo", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "format", + "type": "string", + "required": false, + "default": "json" + }, + { + "name": "default_webhook_url", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "string" + } + } + } + } + }, + "signalwire.core.swml_service": { + "classes": { + "SWMLService": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "name", + "type": "string", + "required": false, + "default": "service" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/" + }, { "name": "host", "type": "string", - "required": true + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "basic_auth", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "schema_path", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "set_name": { + "add_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "section_name", "type": "string", "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "bool" }, - "set_port": { + "add_verb": { "params": [ { "name": "self", "kind": "self" }, { - "name": "port", - "type": "int", + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "config", + "type": "any", "required": true } ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "set_route": { + "add_verb_to_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "route", + "name": "section_name", + "type": "string", + "required": true + }, + { + "name": "verb_name", "type": "string", "required": true + }, + { + "name": "config", + "type": "any", + "required": true } ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "sip_refer": { + "ai": { "params": [ { "name": "self", @@ -8573,30 +8892,73 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "sleep": { + "amazon_bedrock": { "params": [ { "name": "self", "kind": "self" }, { - "name": "milliseconds", - "type": "int", - "required": true + "name": "params", + "type": "any", + "required": false, + "default": null } ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "stop": { + "answer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "as_router": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "class:signalwire.core.web.HostAppRouter" }, - "stop_denoise": { + "auth_password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "auth_username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "build_tool_registry_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "cond": { "params": [ { "name": "self", @@ -8611,7 +8973,7 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "stop_record_call": { + "connect": { "params": [ { "name": "self", @@ -8626,22 +8988,21 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "stop_tap": { + "define_tool": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null + "name": "tool", + "type": "class:signalwire.core.swaig_function.ToolDefinition", + "required": true } ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "switch_section": { + "denoise": { "params": [ { "name": "self", @@ -8656,7 +9017,7 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "tap": { + "detect_machine": { "params": [ { "name": "self", @@ -8671,37 +9032,25 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "timing_safe_compare": { + "document": { "params": [ { - "name": "a", - "type": "string", - "required": true - }, - { - "name": "b", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "bool" + "returns": "class:signalwire.swml.document.Document" }, - "transfer": { + "domain": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "optional" }, - "unset": { + "enter_queue": { "params": [ { "name": "self", @@ -8716,7 +9065,7 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "user_event": { + "execute": { "params": [ { "name": "self", @@ -8731,83 +9080,63 @@ ], "returns": "class:signalwire.core.swml_service.SWMLService" }, - "validate_basic_auth": { + "extract_introspect_payload": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "username", - "type": "string", - "required": true - }, - { - "name": "password", + "name": "stdout_capture", "type": "string", "required": true } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.swml_verbs_generated": { - "classes": { - "AI": { - "methods": { - "__init__": { + "returns": "string" + }, + "extract_sip_username": { "params": [ { - "name": "self", - "kind": "self" + "name": "request_body", + "type": "any", + "required": true } ], - "returns": "void" + "returns": "string" }, - "ai": { + "full_validation_enabled": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" - } - } - }, - "AIObject": { - "methods": { - "SWAIG": { + "returns": "bool" + }, + "get_all_functions": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "dict" }, - "__init__": { + "get_basic_auth_credentials": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "tuple" }, - "hints": { + "get_basic_auth_credentials_with_source": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "tuple" }, - "languages": { + "get_document": { "params": [ { "name": "self", @@ -8816,299 +9145,471 @@ ], "returns": "any" }, - "params": { + "get_function": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.swaig_function.ToolDefinition" }, - "post_prompt": { + "get_routing_callback_paths": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "list" }, - "prompt": { + "goto": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "pronounce": { + "handle_request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "method", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true + }, + { + "name": "body", + "type": "optional", + "required": false, + "default": null } ], - "returns": "any" - } - } - }, - "AIParams": { - "methods": { - "__init__": { + "returns": "tuple,string>" + }, + "hangup": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "acknowledge_interruptions": { + "has_function": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "bool" }, - "ai_volume": { + "has_tool": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "bool" }, - "asr_diarize": { + "host": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "asr_smart_format": { + "join_conference": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "asr_speaker_affinity": { + "join_room": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "attention_timeout": { + "label": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "background_file_loops": { + "list_tool_names": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "list" }, - "background_file_volume": { + "live_transcribe": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "barge_functions": { + "live_translate": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "barge_min_words": { + "manual_set_proxy_url": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "proxy_url", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "void" }, - "conversation_sliding_window": { + "name": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "convo": { + "on_function_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "debug": { + "on_request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", + "required": false, + "default": null } ], - "returns": "any" + "returns": "optional" }, - "debug_webhook_level": { + "on_swml_request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", + "required": false, + "default": null } ], - "returns": "any" + "returns": "optional" }, - "digit_timeout": { + "pay": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - }, - "direction": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "eleven_labs_similarity": { + "play": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "eleven_labs_stability": { + "port": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "int" }, - "enable_barge": { + "prompt": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "enable_inner_dialog": { + "receive_fax": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "enable_pause": { + "record": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "enable_thinking": { + "record_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "enable_turn_detection": { + "register_routing_callback": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "callback", + "type": "class:signalwire.routing_callback.RoutingCallback", + "required": true + }, + { + "name": "path", + "type": "string", + "required": false, + "default": "/sip" } ], - "returns": "any" + "returns": "void" }, - "enable_vision": { + "register_swaig_function": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "func_def", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "end_of_speech_timeout": { + "register_verb_handler": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "handler", + "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", + "required": true } ], - "returns": "any" + "returns": "void" }, - "energy_level": { + "remove_function": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "bool" }, - "first_word_timeout": { + "render_document": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "function_wait_for_talking": { + "render_swml": { "params": [ { "name": "self", @@ -9117,277 +9618,388 @@ ], "returns": "any" }, - "functions_on_no_response": { + "request": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "hard_stop_time": { + "reset_document": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "hold_on_process": { + "return": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "inactivity_timeout": { + "route": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "initial_sleep_ms": { + "schema_utils": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "class:signalwire.utils.schema_utils.SchemaUtils" }, - "inner_dialog_synced": { + "send_digits": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "input_poll_freq": { + "send_fax": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "interrupt_on_noise": { + "send_sms": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "languages_enabled": { + "serve": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "llm_diarize_aware": { + "set": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "max_emotion": { + "set_auth": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "username", + "type": "string", + "required": true + }, + { + "name": "password", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "max_response_tokens": { + "sip_refer": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "outbound_attention_timeout": { + "sleep": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "milliseconds", + "type": "int", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "persist_global_data": { + "ssl_cert_path": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "optional" }, - "save_conversation": { + "ssl_enabled": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "bool" }, - "speak_when_spoken_to": { + "ssl_key_path": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "optional" }, - "speech_event_timeout": { + "stop": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "speech_gen_quick_stops": { + "stop_denoise": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "speech_timeout": { + "stop_record_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "start_paused": { + "stop_tap": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - }, - "static_greeting_no_barge": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "summary_mode": { + "switch": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - }, - "swaig_allow_settings": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "swaig_allow_swml": { + "tap": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "swaig_post_conversation": { + "timing_safe_compare": { "params": [ { - "name": "self", - "kind": "self" + "name": "a", + "type": "string", + "required": true + }, + { + "name": "b", + "type": "string", + "required": true } ], - "returns": "any" + "returns": "bool" }, - "swaig_post_swml_vars": { + "transfer": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "swaig_set_global_data": { + "unset": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "transfer_summary": { + "user_event": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.core.swml_service.SWMLService" }, - "transparent_barge": { + "validate_basic_auth": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "username", + "type": "string", + "required": true + }, + { + "name": "password", + "type": "string", + "required": true } ], - "returns": "any" - }, - "transparent_barge_max_time": { + "returns": "bool" + } + } + } + } + }, + "signalwire.core.swml_verbs_generated": { + "classes": { + "AI": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "turn_detection_timeout": { + "ai": { "params": [ { "name": "self", @@ -9395,8 +10007,12 @@ } ], "returns": "any" - }, - "wait_for_user": { + } + } + }, + "AIObject": { + "methods": { + "SWAIG": { "params": [ { "name": "self", @@ -9404,11 +10020,7 @@ } ], "returns": "any" - } - } - }, - "AIPostPromptPom": { - "methods": { + }, "__init__": { "params": [ { @@ -9418,7 +10030,7 @@ ], "returns": "void" }, - "confidence": { + "hints": { "params": [ { "name": "self", @@ -9427,7 +10039,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "languages": { "params": [ { "name": "self", @@ -9436,7 +10048,7 @@ ], "returns": "any" }, - "pom": { + "params": { "params": [ { "name": "self", @@ -9445,7 +10057,7 @@ ], "returns": "any" }, - "presence_penalty": { + "post_prompt": { "params": [ { "name": "self", @@ -9454,7 +10066,7 @@ ], "returns": "any" }, - "temperature": { + "prompt": { "params": [ { "name": "self", @@ -9463,7 +10075,7 @@ ], "returns": "any" }, - "top_p": { + "pronounce": { "params": [ { "name": "self", @@ -9474,7 +10086,7 @@ } } }, - "AIPostPromptText": { + "AIParams": { "methods": { "__init__": { "params": [ @@ -9485,7 +10097,7 @@ ], "returns": "void" }, - "confidence": { + "acknowledge_interruptions": { "params": [ { "name": "self", @@ -9494,7 +10106,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "ai_volume": { "params": [ { "name": "self", @@ -9503,7 +10115,7 @@ ], "returns": "any" }, - "presence_penalty": { + "asr_diarize": { "params": [ { "name": "self", @@ -9512,7 +10124,7 @@ ], "returns": "any" }, - "temperature": { + "asr_smart_format": { "params": [ { "name": "self", @@ -9521,7 +10133,7 @@ ], "returns": "any" }, - "top_p": { + "asr_speaker_affinity": { "params": [ { "name": "self", @@ -9529,21 +10141,17 @@ } ], "returns": "any" - } - } - }, - "AIPromptPom": { - "methods": { - "__init__": { + }, + "attention_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "confidence": { + "background_file_loops": { "params": [ { "name": "self", @@ -9552,7 +10160,7 @@ ], "returns": "any" }, - "contexts": { + "background_file_volume": { "params": [ { "name": "self", @@ -9561,7 +10169,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "barge_functions": { "params": [ { "name": "self", @@ -9570,7 +10178,7 @@ ], "returns": "any" }, - "pom": { + "barge_min_words": { "params": [ { "name": "self", @@ -9579,7 +10187,7 @@ ], "returns": "any" }, - "presence_penalty": { + "conversation_sliding_window": { "params": [ { "name": "self", @@ -9588,7 +10196,7 @@ ], "returns": "any" }, - "temperature": { + "convo": { "params": [ { "name": "self", @@ -9597,7 +10205,7 @@ ], "returns": "any" }, - "top_p": { + "debug": { "params": [ { "name": "self", @@ -9605,21 +10213,17 @@ } ], "returns": "any" - } - } - }, - "AIPromptText": { - "methods": { - "__init__": { + }, + "debug_webhook_level": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "confidence": { + "digit_timeout": { "params": [ { "name": "self", @@ -9628,7 +10232,7 @@ ], "returns": "any" }, - "contexts": { + "direction": { "params": [ { "name": "self", @@ -9637,7 +10241,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "eleven_labs_similarity": { "params": [ { "name": "self", @@ -9646,7 +10250,7 @@ ], "returns": "any" }, - "presence_penalty": { + "eleven_labs_stability": { "params": [ { "name": "self", @@ -9655,7 +10259,7 @@ ], "returns": "any" }, - "temperature": { + "enable_barge": { "params": [ { "name": "self", @@ -9664,7 +10268,7 @@ ], "returns": "any" }, - "top_p": { + "enable_inner_dialog": { "params": [ { "name": "self", @@ -9672,21 +10276,17 @@ } ], "returns": "any" - } - } - }, - "AllOfProperty": { - "methods": { - "__init__": { + }, + "enable_pause": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "allOf": { + "enable_thinking": { "params": [ { "name": "self", @@ -9694,21 +10294,17 @@ } ], "returns": "any" - } - } - }, - "AmazonBedrock": { - "methods": { - "__init__": { + }, + "enable_turn_detection": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "amazon_bedrock": { + "enable_vision": { "params": [ { "name": "self", @@ -9716,12 +10312,8 @@ } ], "returns": "any" - } - } - }, - "AmazonBedrockObject": { - "methods": { - "SWAIG": { + }, + "end_of_speech_timeout": { "params": [ { "name": "self", @@ -9730,16 +10322,16 @@ ], "returns": "any" }, - "__init__": { + "energy_level": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "params": { + "first_word_timeout": { "params": [ { "name": "self", @@ -9748,7 +10340,7 @@ ], "returns": "any" }, - "post_prompt": { + "function_wait_for_talking": { "params": [ { "name": "self", @@ -9757,7 +10349,7 @@ ], "returns": "any" }, - "prompt": { + "functions_on_no_response": { "params": [ { "name": "self", @@ -9765,21 +10357,17 @@ } ], "returns": "any" - } - } - }, - "AnyOfProperty": { - "methods": { - "__init__": { + }, + "hard_stop_time": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "anyOf": { + "hold_on_process": { "params": [ { "name": "self", @@ -9787,21 +10375,17 @@ } ], "returns": "any" - } - } - }, - "ArrayProperty": { - "methods": { - "__init__": { + }, + "inactivity_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "items": { + "initial_sleep_ms": { "params": [ { "name": "self", @@ -9810,7 +10394,7 @@ ], "returns": "any" }, - "nullable": { + "inner_dialog_synced": { "params": [ { "name": "self", @@ -9818,21 +10402,17 @@ } ], "returns": "any" - } - } - }, - "BedrockParams": { - "methods": { - "__init__": { + }, + "input_poll_freq": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "attention_timeout": { + "interrupt_on_noise": { "params": [ { "name": "self", @@ -9841,7 +10421,7 @@ ], "returns": "any" }, - "hard_stop_time": { + "languages_enabled": { "params": [ { "name": "self", @@ -9850,7 +10430,7 @@ ], "returns": "any" }, - "inactivity_timeout": { + "llm_diarize_aware": { "params": [ { "name": "self", @@ -9858,21 +10438,17 @@ } ], "returns": "any" - } - } - }, - "BedrockSWAIG": { - "methods": { - "__init__": { + }, + "max_emotion": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "defaults": { + "max_response_tokens": { "params": [ { "name": "self", @@ -9881,7 +10457,7 @@ ], "returns": "any" }, - "functions": { + "outbound_attention_timeout": { "params": [ { "name": "self", @@ -9890,7 +10466,7 @@ ], "returns": "any" }, - "includes": { + "persist_global_data": { "params": [ { "name": "self", @@ -9899,7 +10475,7 @@ ], "returns": "any" }, - "native_functions": { + "save_conversation": { "params": [ { "name": "self", @@ -9907,21 +10483,17 @@ } ], "returns": "any" - } - } - }, - "BooleanProperty": { - "methods": { - "__init__": { + }, + "speak_when_spoken_to": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "speech_event_timeout": { "params": [ { "name": "self", @@ -9930,7 +10502,7 @@ ], "returns": "any" }, - "nullable": { + "speech_gen_quick_stops": { "params": [ { "name": "self", @@ -9938,21 +10510,17 @@ } ], "returns": "any" - } - } - }, - "Cond": { - "methods": { - "__init__": { + }, + "speech_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "cond": { + "start_paused": { "params": [ { "name": "self", @@ -9960,21 +10528,17 @@ } ], "returns": "any" - } - } - }, - "CondReg": { - "methods": { - "__init__": { + }, + "static_greeting_no_barge": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "then": { + "summary_mode": { "params": [ { "name": "self", @@ -9982,21 +10546,17 @@ } ], "returns": "any" - } - } - }, - "Connect": { - "methods": { - "__init__": { + }, + "swaig_allow_settings": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "connect": { + "swaig_allow_swml": { "params": [ { "name": "self", @@ -10004,21 +10564,17 @@ } ], "returns": "any" - } - } - }, - "ConnectConfig": { - "methods": { - "__init__": { + }, + "swaig_post_conversation": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "answer_on_bridge": { + "swaig_post_swml_vars": { "params": [ { "name": "self", @@ -10027,7 +10583,7 @@ ], "returns": "any" }, - "call_state_events": { + "swaig_set_global_data": { "params": [ { "name": "self", @@ -10036,7 +10592,7 @@ ], "returns": "any" }, - "confirm": { + "transfer_summary": { "params": [ { "name": "self", @@ -10045,7 +10601,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "transparent_barge": { "params": [ { "name": "self", @@ -10054,7 +10610,7 @@ ], "returns": "any" }, - "headers": { + "transparent_barge_max_time": { "params": [ { "name": "self", @@ -10063,7 +10619,7 @@ ], "returns": "any" }, - "max_duration": { + "turn_detection_timeout": { "params": [ { "name": "self", @@ -10072,7 +10628,7 @@ ], "returns": "any" }, - "parallel": { + "wait_for_user": { "params": [ { "name": "self", @@ -10080,17 +10636,21 @@ } ], "returns": "any" - }, - "result": { + } + } + }, + "AIPostPromptPom": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "serial": { + "confidence": { "params": [ { "name": "self", @@ -10099,7 +10659,7 @@ ], "returns": "any" }, - "serial_parallel": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10108,7 +10668,7 @@ ], "returns": "any" }, - "session_timeout": { + "pom": { "params": [ { "name": "self", @@ -10117,7 +10677,7 @@ ], "returns": "any" }, - "timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -10126,7 +10686,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "temperature": { "params": [ { "name": "self", @@ -10135,7 +10695,7 @@ ], "returns": "any" }, - "webrtc_media": { + "top_p": { "params": [ { "name": "self", @@ -10146,7 +10706,7 @@ } } }, - "ConnectDeviceParallel": { + "AIPostPromptText": { "methods": { "__init__": { "params": [ @@ -10157,7 +10717,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "confidence": { "params": [ { "name": "self", @@ -10166,7 +10726,7 @@ ], "returns": "any" }, - "call_state_events": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10175,7 +10735,7 @@ ], "returns": "any" }, - "confirm": { + "presence_penalty": { "params": [ { "name": "self", @@ -10184,7 +10744,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "temperature": { "params": [ { "name": "self", @@ -10193,7 +10753,7 @@ ], "returns": "any" }, - "headers": { + "top_p": { "params": [ { "name": "self", @@ -10201,8 +10761,21 @@ } ], "returns": "any" + } + } + }, + "AIPromptPom": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "max_duration": { + "confidence": { "params": [ { "name": "self", @@ -10211,7 +10784,7 @@ ], "returns": "any" }, - "parallel": { + "contexts": { "params": [ { "name": "self", @@ -10220,7 +10793,7 @@ ], "returns": "any" }, - "result": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10229,7 +10802,7 @@ ], "returns": "any" }, - "session_timeout": { + "pom": { "params": [ { "name": "self", @@ -10238,7 +10811,7 @@ ], "returns": "any" }, - "timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -10247,7 +10820,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "temperature": { "params": [ { "name": "self", @@ -10256,7 +10829,7 @@ ], "returns": "any" }, - "webrtc_media": { + "top_p": { "params": [ { "name": "self", @@ -10267,7 +10840,7 @@ } } }, - "ConnectDeviceSerial": { + "AIPromptText": { "methods": { "__init__": { "params": [ @@ -10278,7 +10851,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "confidence": { "params": [ { "name": "self", @@ -10287,7 +10860,7 @@ ], "returns": "any" }, - "call_state_events": { + "contexts": { "params": [ { "name": "self", @@ -10296,7 +10869,7 @@ ], "returns": "any" }, - "confirm": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10305,7 +10878,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -10314,7 +10887,7 @@ ], "returns": "any" }, - "headers": { + "temperature": { "params": [ { "name": "self", @@ -10323,7 +10896,7 @@ ], "returns": "any" }, - "max_duration": { + "top_p": { "params": [ { "name": "self", @@ -10331,8 +10904,12 @@ } ], "returns": "any" - }, - "result": { + } + } + }, + "AiSidecarConfig": { + "methods": { + "SWAIG": { "params": [ { "name": "self", @@ -10341,25 +10918,29 @@ ], "returns": "any" }, - "serial": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" - }, - "session_timeout": { + "returns": "void" + } + } + }, + "AllOfProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "timeout": { + "allOf": { "params": [ { "name": "self", @@ -10367,17 +10948,21 @@ } ], "returns": "any" - }, - "transfer_after_bridge": { + } + } + }, + "AmazonBedrock": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "webrtc_media": { + "amazon_bedrock": { "params": [ { "name": "self", @@ -10388,27 +10973,27 @@ } } }, - "ConnectDeviceSerialParallel": { + "AmazonBedrockObject": { "methods": { - "__init__": { + "SWAIG": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "answer_on_bridge": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "call_state_events": { + "params": { "params": [ { "name": "self", @@ -10417,7 +11002,7 @@ ], "returns": "any" }, - "confirm": { + "post_prompt": { "params": [ { "name": "self", @@ -10426,7 +11011,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "prompt": { "params": [ { "name": "self", @@ -10434,17 +11019,21 @@ } ], "returns": "any" - }, - "headers": { + } + } + }, + "AnyOfProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "max_duration": { + "anyOf": { "params": [ { "name": "self", @@ -10452,17 +11041,21 @@ } ], "returns": "any" - }, - "result": { + } + } + }, + "ArrayProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "serial_parallel": { + "items": { "params": [ { "name": "self", @@ -10471,7 +11064,7 @@ ], "returns": "any" }, - "session_timeout": { + "nullable": { "params": [ { "name": "self", @@ -10479,8 +11072,21 @@ } ], "returns": "any" + } + } + }, + "BedrockParams": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "timeout": { + "attention_timeout": { "params": [ { "name": "self", @@ -10489,7 +11095,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "hard_stop_time": { "params": [ { "name": "self", @@ -10498,7 +11104,7 @@ ], "returns": "any" }, - "webrtc_media": { + "inactivity_timeout": { "params": [ { "name": "self", @@ -10509,7 +11115,7 @@ } } }, - "ConnectDeviceSingle": { + "BedrockSWAIG": { "methods": { "__init__": { "params": [ @@ -10520,7 +11126,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "defaults": { "params": [ { "name": "self", @@ -10529,7 +11135,7 @@ ], "returns": "any" }, - "call_state_events": { + "functions": { "params": [ { "name": "self", @@ -10538,7 +11144,7 @@ ], "returns": "any" }, - "confirm": { + "includes": { "params": [ { "name": "self", @@ -10547,7 +11153,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "native_functions": { "params": [ { "name": "self", @@ -10555,17 +11161,21 @@ } ], "returns": "any" - }, - "headers": { + } + } + }, + "BooleanProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "max_duration": { + "default": { "params": [ { "name": "self", @@ -10574,7 +11184,7 @@ ], "returns": "any" }, - "result": { + "nullable": { "params": [ { "name": "self", @@ -10582,17 +11192,21 @@ } ], "returns": "any" - }, - "session_timeout": { + } + } + }, + "Cond": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "timeout": { + "cond": { "params": [ { "name": "self", @@ -10600,17 +11214,21 @@ } ], "returns": "any" - }, - "transfer_after_bridge": { + } + } + }, + "CondReg": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "webrtc_media": { + "then": { "params": [ { "name": "self", @@ -10621,7 +11239,7 @@ } } }, - "ConnectSwitch": { + "Connect": { "methods": { "__init__": { "params": [ @@ -10632,7 +11250,7 @@ ], "returns": "void" }, - "default": { + "connect": { "params": [ { "name": "self", @@ -10643,7 +11261,7 @@ } } }, - "ContextPOMSteps": { + "ConnectConfig": { "methods": { "__init__": { "params": [ @@ -10654,7 +11272,7 @@ ], "returns": "void" }, - "pom": { + "answer_on_bridge": { "params": [ { "name": "self", @@ -10663,7 +11281,7 @@ ], "returns": "any" }, - "skip_user_turn": { + "call_state_events": { "params": [ { "name": "self", @@ -10671,21 +11289,17 @@ } ], "returns": "any" - } - } - }, - "ContextTextSteps": { - "methods": { - "__init__": { + }, + "confirm": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "skip_user_turn": { + "confirm_timeout": { "params": [ { "name": "self", @@ -10693,21 +11307,17 @@ } ], "returns": "any" - } - } - }, - "Contexts": { - "methods": { - "__init__": { + }, + "headers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "max_duration": { "params": [ { "name": "self", @@ -10715,21 +11325,17 @@ } ], "returns": "any" - } - } - }, - "ContextsPOMObject": { - "methods": { - "__init__": { + }, + "parallel": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "enter_fillers": { + "result": { "params": [ { "name": "self", @@ -10738,7 +11344,7 @@ ], "returns": "any" }, - "exit_fillers": { + "ringback": { "params": [ { "name": "self", @@ -10747,7 +11353,7 @@ ], "returns": "any" }, - "pom": { + "serial": { "params": [ { "name": "self", @@ -10756,7 +11362,7 @@ ], "returns": "any" }, - "steps": { + "serial_parallel": { "params": [ { "name": "self", @@ -10764,21 +11370,17 @@ } ], "returns": "any" - } - } - }, - "ContextsTextObject": { - "methods": { - "__init__": { + }, + "session_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "enter_fillers": { + "timeout": { "params": [ { "name": "self", @@ -10787,7 +11389,7 @@ ], "returns": "any" }, - "exit_fillers": { + "transfer_after_bridge": { "params": [ { "name": "self", @@ -10796,7 +11398,7 @@ ], "returns": "any" }, - "steps": { + "webrtc_media": { "params": [ { "name": "self", @@ -10807,7 +11409,7 @@ } } }, - "ConversationMessage": { + "ConnectDeviceParallel": { "methods": { "__init__": { "params": [ @@ -10818,7 +11420,7 @@ ], "returns": "void" }, - "role": { + "answer_on_bridge": { "params": [ { "name": "self", @@ -10826,21 +11428,8 @@ } ], "returns": "any" - } - } - }, - "DataMap": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "expressions": { + "call_state_events": { "params": [ { "name": "self", @@ -10849,7 +11438,7 @@ ], "returns": "any" }, - "output": { + "confirm": { "params": [ { "name": "self", @@ -10858,7 +11447,7 @@ ], "returns": "any" }, - "webhooks": { + "confirm_timeout": { "params": [ { "name": "self", @@ -10866,21 +11455,17 @@ } ], "returns": "any" - } - } - }, - "DetectMachineConfig": { - "methods": { - "__init__": { + }, + "headers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "detect_message_end": { + "max_duration": { "params": [ { "name": "self", @@ -10889,7 +11474,7 @@ ], "returns": "any" }, - "end_silence_timeout": { + "parallel": { "params": [ { "name": "self", @@ -10898,7 +11483,7 @@ ], "returns": "any" }, - "initial_timeout": { + "result": { "params": [ { "name": "self", @@ -10907,7 +11492,7 @@ ], "returns": "any" }, - "machine_ready_timeout": { + "ringback": { "params": [ { "name": "self", @@ -10916,7 +11501,7 @@ ], "returns": "any" }, - "machine_voice_threshold": { + "session_timeout": { "params": [ { "name": "self", @@ -10925,7 +11510,7 @@ ], "returns": "any" }, - "machine_words_threshold": { + "timeout": { "params": [ { "name": "self", @@ -10934,7 +11519,7 @@ ], "returns": "any" }, - "timeout": { + "transfer_after_bridge": { "params": [ { "name": "self", @@ -10943,7 +11528,7 @@ ], "returns": "any" }, - "wait": { + "webrtc_media": { "params": [ { "name": "self", @@ -10954,7 +11539,7 @@ } } }, - "EnterQueue": { + "ConnectDeviceSerial": { "methods": { "__init__": { "params": [ @@ -10965,7 +11550,7 @@ ], "returns": "void" }, - "enter_queue": { + "answer_on_bridge": { "params": [ { "name": "self", @@ -10973,21 +11558,17 @@ } ], "returns": "any" - } - } - }, - "EnterQueueObject": { - "methods": { - "__init__": { + }, + "call_state_events": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "transfer_after_bridge": { + "confirm": { "params": [ { "name": "self", @@ -10996,7 +11577,7 @@ ], "returns": "any" }, - "wait_time": { + "confirm_timeout": { "params": [ { "name": "self", @@ -11005,7 +11586,7 @@ ], "returns": "any" }, - "wait_url": { + "headers": { "params": [ { "name": "self", @@ -11013,21 +11594,17 @@ } ], "returns": "any" - } - } - }, - "ExecuteConfig": { - "methods": { - "__init__": { + }, + "max_duration": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "on_return": { + "result": { "params": [ { "name": "self", @@ -11036,7 +11613,7 @@ ], "returns": "any" }, - "result": { + "ringback": { "params": [ { "name": "self", @@ -11044,21 +11621,17 @@ } ], "returns": "any" - } - } - }, - "ExecuteSwitch": { - "methods": { - "__init__": { + }, + "serial": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "session_timeout": { "params": [ { "name": "self", @@ -11066,21 +11639,26 @@ } ], "returns": "any" - } - } - }, - "Expression": { - "methods": { - "__init__": { + }, + "timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "output": { + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { "params": [ { "name": "self", @@ -11091,7 +11669,7 @@ } } }, - "GotoConfig": { + "ConnectDeviceSerialParallel": { "methods": { "__init__": { "params": [ @@ -11102,7 +11680,7 @@ ], "returns": "void" }, - "max": { + "answer_on_bridge": { "params": [ { "name": "self", @@ -11110,21 +11688,17 @@ } ], "returns": "any" - } - } - }, - "HangUpHookSWAIGFunction": { - "methods": { - "__init__": { + }, + "call_state_events": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "active": { + "confirm": { "params": [ { "name": "self", @@ -11133,7 +11707,7 @@ ], "returns": "any" }, - "argument": { + "confirm_timeout": { "params": [ { "name": "self", @@ -11142,7 +11716,7 @@ ], "returns": "any" }, - "data_map": { + "headers": { "params": [ { "name": "self", @@ -11151,7 +11725,7 @@ ], "returns": "any" }, - "fillers": { + "max_duration": { "params": [ { "name": "self", @@ -11160,7 +11734,7 @@ ], "returns": "any" }, - "parameters": { + "result": { "params": [ { "name": "self", @@ -11169,7 +11743,7 @@ ], "returns": "any" }, - "skip_fillers": { + "ringback": { "params": [ { "name": "self", @@ -11178,7 +11752,7 @@ ], "returns": "any" }, - "wait_for_fillers": { + "serial_parallel": { "params": [ { "name": "self", @@ -11186,21 +11760,17 @@ } ], "returns": "any" - } - } - }, - "HangupAction": { - "methods": { - "__init__": { + }, + "session_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "hangup": { + "timeout": { "params": [ { "name": "self", @@ -11208,21 +11778,17 @@ } ], "returns": "any" - } - } - }, - "Hint": { - "methods": { - "__init__": { + }, + "transfer_after_bridge": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "ignore_case": { + "webrtc_media": { "params": [ { "name": "self", @@ -11233,7 +11799,7 @@ } } }, - "HoldAction": { + "ConnectDeviceSingle": { "methods": { "__init__": { "params": [ @@ -11244,7 +11810,7 @@ ], "returns": "void" }, - "hold": { + "answer_on_bridge": { "params": [ { "name": "self", @@ -11252,21 +11818,17 @@ } ], "returns": "any" - } - } - }, - "IntegerProperty": { - "methods": { - "__init__": { + }, + "call_state_events": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "confirm": { "params": [ { "name": "self", @@ -11275,7 +11837,7 @@ ], "returns": "any" }, - "nullable": { + "confirm_timeout": { "params": [ { "name": "self", @@ -11283,21 +11845,17 @@ } ], "returns": "any" - } - } - }, - "JoinConference": { - "methods": { - "__init__": { + }, + "headers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "join_conference": { + "max_duration": { "params": [ { "name": "self", @@ -11305,21 +11863,17 @@ } ], "returns": "any" - } - } - }, - "JoinConferenceObject": { - "methods": { - "__init__": { + }, + "result": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "end_on_exit": { + "ringback": { "params": [ { "name": "self", @@ -11328,7 +11882,7 @@ ], "returns": "any" }, - "max_participants": { + "session_timeout": { "params": [ { "name": "self", @@ -11337,7 +11891,7 @@ ], "returns": "any" }, - "muted": { + "timeout": { "params": [ { "name": "self", @@ -11346,7 +11900,7 @@ ], "returns": "any" }, - "result": { + "transfer_after_bridge": { "params": [ { "name": "self", @@ -11355,7 +11909,7 @@ ], "returns": "any" }, - "start_on_enter": { + "webrtc_media": { "params": [ { "name": "self", @@ -11363,8 +11917,21 @@ } ], "returns": "any" + } + } + }, + "ConnectSwitch": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "wait_url": { + "default": { "params": [ { "name": "self", @@ -11375,7 +11942,7 @@ } } }, - "LanguageParams": { + "ContextPOMSteps": { "methods": { "__init__": { "params": [ @@ -11386,7 +11953,7 @@ ], "returns": "void" }, - "similarity": { + "pom": { "params": [ { "name": "self", @@ -11395,7 +11962,7 @@ ], "returns": "any" }, - "stability": { + "skip_user_turn": { "params": [ { "name": "self", @@ -11406,7 +11973,7 @@ } } }, - "LanguagesWithFillers": { + "ContextTextSteps": { "methods": { "__init__": { "params": [ @@ -11417,7 +11984,7 @@ ], "returns": "void" }, - "params": { + "skip_user_turn": { "params": [ { "name": "self", @@ -11428,7 +11995,7 @@ } } }, - "LanguagesWithSoloFillers": { + "Contexts": { "methods": { "__init__": { "params": [ @@ -11439,7 +12006,7 @@ ], "returns": "void" }, - "params": { + "default": { "params": [ { "name": "self", @@ -11450,7 +12017,7 @@ } } }, - "LiveTranscribeConfig": { + "ContextsPOMObject": { "methods": { "__init__": { "params": [ @@ -11461,7 +12028,7 @@ ], "returns": "void" }, - "action": { + "enter_fillers": { "params": [ { "name": "self", @@ -11469,21 +12036,26 @@ } ], "returns": "any" - } - } - }, - "LiveTranslateConfig": { - "methods": { - "__init__": { + }, + "exit_fillers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "action": { + "pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "steps": { "params": [ { "name": "self", @@ -11494,7 +12066,7 @@ } } }, - "NumberProperty": { + "ContextsTextObject": { "methods": { "__init__": { "params": [ @@ -11505,7 +12077,7 @@ ], "returns": "void" }, - "default": { + "enter_fillers": { "params": [ { "name": "self", @@ -11514,7 +12086,7 @@ ], "returns": "any" }, - "enum": { + "exit_fillers": { "params": [ { "name": "self", @@ -11523,7 +12095,7 @@ ], "returns": "any" }, - "nullable": { + "steps": { "params": [ { "name": "self", @@ -11534,7 +12106,7 @@ } } }, - "ObjectProperty": { + "ConversationMessage": { "methods": { "__init__": { "params": [ @@ -11545,7 +12117,7 @@ ], "returns": "void" }, - "nullable": { + "role": { "params": [ { "name": "self", @@ -11556,7 +12128,7 @@ } } }, - "OmitPropertiesBedrockPostPomptTextOmittedPromptProps": { + "DataMap": { "methods": { "__init__": { "params": [ @@ -11567,7 +12139,7 @@ ], "returns": "void" }, - "confidence": { + "expressions": { "params": [ { "name": "self", @@ -11576,7 +12148,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "output": { "params": [ { "name": "self", @@ -11585,7 +12157,7 @@ ], "returns": "any" }, - "presence_penalty": { + "webhooks": { "params": [ { "name": "self", @@ -11593,17 +12165,21 @@ } ], "returns": "any" - }, - "temperature": { + } + } + }, + "DetectMachineConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "top_p": { + "detect_message_end": { "params": [ { "name": "self", @@ -11611,21 +12187,17 @@ } ], "returns": "any" - } - } - }, - "OmitPropertiesBedrockPostPromptPomOmittedPromptProps": { - "methods": { - "__init__": { + }, + "end_silence_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "confidence": { + "initial_timeout": { "params": [ { "name": "self", @@ -11634,7 +12206,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "machine_ready_timeout": { "params": [ { "name": "self", @@ -11643,7 +12215,7 @@ ], "returns": "any" }, - "pom": { + "machine_voice_threshold": { "params": [ { "name": "self", @@ -11652,7 +12224,7 @@ ], "returns": "any" }, - "presence_penalty": { + "machine_words_threshold": { "params": [ { "name": "self", @@ -11661,7 +12233,7 @@ ], "returns": "any" }, - "temperature": { + "timeout": { "params": [ { "name": "self", @@ -11670,7 +12242,7 @@ ], "returns": "any" }, - "top_p": { + "wait": { "params": [ { "name": "self", @@ -11681,7 +12253,7 @@ } } }, - "OmitPropertiesBedrockPromptPomOmittedPromptProps": { + "EnterQueue": { "methods": { "__init__": { "params": [ @@ -11692,16 +12264,7 @@ ], "returns": "void" }, - "confidence": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "frequency_penalty": { + "enter_queue": { "params": [ { "name": "self", @@ -11709,17 +12272,21 @@ } ], "returns": "any" - }, - "pom": { + } + } + }, + "EnterQueueObject": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "presence_penalty": { + "transfer_after_bridge": { "params": [ { "name": "self", @@ -11728,7 +12295,7 @@ ], "returns": "any" }, - "temperature": { + "wait_time": { "params": [ { "name": "self", @@ -11737,7 +12304,7 @@ ], "returns": "any" }, - "top_p": { + "wait_url": { "params": [ { "name": "self", @@ -11748,7 +12315,7 @@ } } }, - "OmitPropertiesBedrockPromptTextOmittedPromptProps": { + "ExecuteConfig": { "methods": { "__init__": { "params": [ @@ -11759,7 +12326,7 @@ ], "returns": "void" }, - "confidence": { + "on_return": { "params": [ { "name": "self", @@ -11768,7 +12335,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "result": { "params": [ { "name": "self", @@ -11776,26 +12343,21 @@ } ], "returns": "any" - }, - "presence_penalty": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "temperature": { + } + } + }, + "ExecuteSwitch": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "top_p": { + "default": { "params": [ { "name": "self", @@ -11806,7 +12368,7 @@ } } }, - "OneOfProperty": { + "Expression": { "methods": { "__init__": { "params": [ @@ -11817,7 +12379,7 @@ ], "returns": "void" }, - "oneOf": { + "output": { "params": [ { "name": "self", @@ -11828,7 +12390,7 @@ } } }, - "Output": { + "GotoConfig": { "methods": { "__init__": { "params": [ @@ -11839,7 +12401,7 @@ ], "returns": "void" }, - "action": { + "max": { "params": [ { "name": "self", @@ -11850,7 +12412,7 @@ } } }, - "PayConfig": { + "HangUpHookSWAIGFunction": { "methods": { "__init__": { "params": [ @@ -11861,7 +12423,7 @@ ], "returns": "void" }, - "max_attempts": { + "active": { "params": [ { "name": "self", @@ -11870,7 +12432,7 @@ ], "returns": "any" }, - "min_postal_code_length": { + "argument": { "params": [ { "name": "self", @@ -11879,7 +12441,7 @@ ], "returns": "any" }, - "parameters": { + "data_map": { "params": [ { "name": "self", @@ -11888,7 +12450,7 @@ ], "returns": "any" }, - "prompts": { + "fillers": { "params": [ { "name": "self", @@ -11897,7 +12459,7 @@ ], "returns": "any" }, - "security_code": { + "parameters": { "params": [ { "name": "self", @@ -11906,7 +12468,16 @@ ], "returns": "any" }, - "timeout": { + "skip_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait_for_fillers": { "params": [ { "name": "self", @@ -11917,7 +12488,7 @@ } } }, - "PayPrompts": { + "HangupAction": { "methods": { "__init__": { "params": [ @@ -11928,7 +12499,7 @@ ], "returns": "void" }, - "actions": { + "hangup": { "params": [ { "name": "self", @@ -11939,7 +12510,7 @@ } } }, - "PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "Hint": { "methods": { "__init__": { "params": [ @@ -11950,7 +12521,7 @@ ], "returns": "void" }, - "active": { + "ignore_case": { "params": [ { "name": "self", @@ -11958,17 +12529,21 @@ } ], "returns": "any" - }, - "data_map": { + } + } + }, + "HoldAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "parameters": { + "hold": { "params": [ { "name": "self", @@ -11979,7 +12554,7 @@ } } }, - "PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "IntegerProperty": { "methods": { "__init__": { "params": [ @@ -11990,7 +12565,7 @@ ], "returns": "void" }, - "active": { + "default": { "params": [ { "name": "self", @@ -11999,7 +12574,7 @@ ], "returns": "any" }, - "data_map": { + "nullable": { "params": [ { "name": "self", @@ -12007,8 +12582,21 @@ } ], "returns": "any" + } + } + }, + "JoinConference": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "parameters": { + "join_conference": { "params": [ { "name": "self", @@ -12019,7 +12607,7 @@ } } }, - "PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps": { + "JoinConferenceObject": { "methods": { "__init__": { "params": [ @@ -12030,7 +12618,7 @@ ], "returns": "void" }, - "active": { + "end_on_exit": { "params": [ { "name": "self", @@ -12039,7 +12627,7 @@ ], "returns": "any" }, - "data_map": { + "max_participants": { "params": [ { "name": "self", @@ -12048,7 +12636,7 @@ ], "returns": "any" }, - "parameters": { + "muted": { "params": [ { "name": "self", @@ -12056,21 +12644,8 @@ } ], "returns": "any" - } - } - }, - "PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "active": { + "result": { "params": [ { "name": "self", @@ -12079,7 +12654,7 @@ ], "returns": "any" }, - "data_map": { + "start_on_enter": { "params": [ { "name": "self", @@ -12088,7 +12663,7 @@ ], "returns": "any" }, - "parameters": { + "wait_url": { "params": [ { "name": "self", @@ -12099,7 +12674,7 @@ } } }, - "Play": { + "LanguageParams": { "methods": { "__init__": { "params": [ @@ -12110,7 +12685,16 @@ ], "returns": "void" }, - "play": { + "similarity": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "stability": { "params": [ { "name": "self", @@ -12121,7 +12705,7 @@ } } }, - "PlayWithURL": { + "LanguagesWithFillers": { "methods": { "__init__": { "params": [ @@ -12132,7 +12716,7 @@ ], "returns": "void" }, - "auto_answer": { + "params": { "params": [ { "name": "self", @@ -12140,17 +12724,21 @@ } ], "returns": "any" - }, - "url": { + } + } + }, + "LanguagesWithSoloFillers": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "volume": { + "params": { "params": [ { "name": "self", @@ -12161,7 +12749,7 @@ } } }, - "PlayWithURLS": { + "LiveTranscribeConfig": { "methods": { "__init__": { "params": [ @@ -12172,7 +12760,7 @@ ], "returns": "void" }, - "auto_answer": { + "action": { "params": [ { "name": "self", @@ -12180,17 +12768,21 @@ } ], "returns": "any" - }, - "urls": { + } + } + }, + "LiveTranslateConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "volume": { + "action": { "params": [ { "name": "self", @@ -12201,7 +12793,7 @@ } } }, - "PomSectionBodyContent": { + "NumberProperty": { "methods": { "__init__": { "params": [ @@ -12212,7 +12804,7 @@ ], "returns": "void" }, - "numbered": { + "default": { "params": [ { "name": "self", @@ -12221,7 +12813,7 @@ ], "returns": "any" }, - "numberedBullets": { + "enum": { "params": [ { "name": "self", @@ -12230,7 +12822,7 @@ ], "returns": "any" }, - "subsections": { + "nullable": { "params": [ { "name": "self", @@ -12241,7 +12833,7 @@ } } }, - "PomSectionBulletsContent": { + "ObjectProperty": { "methods": { "__init__": { "params": [ @@ -12252,7 +12844,7 @@ ], "returns": "void" }, - "numbered": { + "nullable": { "params": [ { "name": "self", @@ -12260,17 +12852,21 @@ } ], "returns": "any" - }, - "numberedBullets": { + } + } + }, + "OmitPropertiesBedrockPostPomptTextOmittedPromptProps": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "subsections": { + "confidence": { "params": [ { "name": "self", @@ -12278,21 +12874,17 @@ } ], "returns": "any" - } - } - }, - "PromptConfig": { - "methods": { - "__init__": { + }, + "frequency_penalty": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "digit_timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -12301,7 +12893,7 @@ ], "returns": "any" }, - "initial_timeout": { + "temperature": { "params": [ { "name": "self", @@ -12310,7 +12902,7 @@ ], "returns": "any" }, - "max_digits": { + "top_p": { "params": [ { "name": "self", @@ -12318,8 +12910,21 @@ } ], "returns": "any" + } + } + }, + "OmitPropertiesBedrockPostPromptPomOmittedPromptProps": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "play": { + "confidence": { "params": [ { "name": "self", @@ -12328,7 +12933,7 @@ ], "returns": "any" }, - "speech_end_timeout": { + "frequency_penalty": { "params": [ { "name": "self", @@ -12337,7 +12942,7 @@ ], "returns": "any" }, - "speech_hints": { + "pom": { "params": [ { "name": "self", @@ -12346,7 +12951,7 @@ ], "returns": "any" }, - "speech_timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -12354,21 +12959,17 @@ } ], "returns": "any" - } - } - }, - "Pronounce": { - "methods": { - "__init__": { + }, + "temperature": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "ignore_case": { + "top_p": { "params": [ { "name": "self", @@ -12379,7 +12980,7 @@ } } }, - "RecordCallConfig": { + "OmitPropertiesBedrockPromptPomOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -12390,7 +12991,7 @@ ], "returns": "void" }, - "beep": { + "confidence": { "params": [ { "name": "self", @@ -12399,7 +13000,7 @@ ], "returns": "any" }, - "end_silence_timeout": { + "frequency_penalty": { "params": [ { "name": "self", @@ -12408,7 +13009,7 @@ ], "returns": "any" }, - "initial_timeout": { + "pom": { "params": [ { "name": "self", @@ -12417,7 +13018,7 @@ ], "returns": "any" }, - "input_sensitivity": { + "presence_penalty": { "params": [ { "name": "self", @@ -12426,7 +13027,7 @@ ], "returns": "any" }, - "max_length": { + "temperature": { "params": [ { "name": "self", @@ -12435,7 +13036,7 @@ ], "returns": "any" }, - "stereo": { + "top_p": { "params": [ { "name": "self", @@ -12446,7 +13047,7 @@ } } }, - "RecordConfig": { + "OmitPropertiesBedrockPromptTextOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -12457,7 +13058,7 @@ ], "returns": "void" }, - "beep": { + "confidence": { "params": [ { "name": "self", @@ -12466,7 +13067,7 @@ ], "returns": "any" }, - "end_silence_timeout": { + "frequency_penalty": { "params": [ { "name": "self", @@ -12475,7 +13076,7 @@ ], "returns": "any" }, - "initial_timeout": { + "presence_penalty": { "params": [ { "name": "self", @@ -12484,16 +13085,7 @@ ], "returns": "any" }, - "input_sensitivity": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "max_length": { + "temperature": { "params": [ { "name": "self", @@ -12502,7 +13094,7 @@ ], "returns": "any" }, - "stereo": { + "top_p": { "params": [ { "name": "self", @@ -12513,7 +13105,7 @@ } } }, - "RequestConfig": { + "OneOfProperty": { "methods": { "__init__": { "params": [ @@ -12524,7 +13116,7 @@ ], "returns": "void" }, - "connect_timeout": { + "oneOf": { "params": [ { "name": "self", @@ -12532,17 +13124,21 @@ } ], "returns": "any" - }, - "save_variables": { + } + } + }, + "Output": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "timeout": { + "action": { "params": [ { "name": "self", @@ -12553,7 +13149,7 @@ } } }, - "SWAIG": { + "PayConfig": { "methods": { "__init__": { "params": [ @@ -12564,7 +13160,7 @@ ], "returns": "void" }, - "defaults": { + "max_attempts": { "params": [ { "name": "self", @@ -12573,7 +13169,7 @@ ], "returns": "any" }, - "functions": { + "min_postal_code_length": { "params": [ { "name": "self", @@ -12582,7 +13178,7 @@ ], "returns": "any" }, - "includes": { + "parameters": { "params": [ { "name": "self", @@ -12591,7 +13187,7 @@ ], "returns": "any" }, - "internal_fillers": { + "prompts": { "params": [ { "name": "self", @@ -12600,7 +13196,16 @@ ], "returns": "any" }, - "native_functions": { + "security_code": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { "params": [ { "name": "self", @@ -12611,7 +13216,7 @@ } } }, - "SWAIGInternalFiller": { + "PayPrompts": { "methods": { "__init__": { "params": [ @@ -12622,7 +13227,7 @@ ], "returns": "void" }, - "adjust_response_latency": { + "actions": { "params": [ { "name": "self", @@ -12630,17 +13235,21 @@ } ], "returns": "any" - }, - "change_context": { + } + } + }, + "PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "check_time": { + "active": { "params": [ { "name": "self", @@ -12649,7 +13258,7 @@ ], "returns": "any" }, - "get_ideal_strategy": { + "data_map": { "params": [ { "name": "self", @@ -12658,7 +13267,7 @@ ], "returns": "any" }, - "get_visual_input": { + "parameters": { "params": [ { "name": "self", @@ -12666,17 +13275,21 @@ } ], "returns": "any" - }, - "hangup": { + } + } + }, + "PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "next_step": { + "active": { "params": [ { "name": "self", @@ -12685,7 +13298,7 @@ ], "returns": "any" }, - "wait_for_user": { + "data_map": { "params": [ { "name": "self", @@ -12694,7 +13307,7 @@ ], "returns": "any" }, - "wait_seconds": { + "parameters": { "params": [ { "name": "self", @@ -12705,7 +13318,7 @@ } } }, - "Section": { + "PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps": { "methods": { "__init__": { "params": [ @@ -12716,7 +13329,25 @@ ], "returns": "void" }, - "main": { + "active": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "data_map": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "parameters": { "params": [ { "name": "self", @@ -12727,7 +13358,7 @@ } } }, - "SendSMS": { + "PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps": { "methods": { "__init__": { "params": [ @@ -12738,7 +13369,25 @@ ], "returns": "void" }, - "send_sms": { + "active": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "data_map": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "parameters": { "params": [ { "name": "self", @@ -12749,7 +13398,7 @@ } } }, - "Sleep": { + "Play": { "methods": { "__init__": { "params": [ @@ -12760,7 +13409,7 @@ ], "returns": "void" }, - "sleep": { + "play": { "params": [ { "name": "self", @@ -12771,7 +13420,7 @@ } } }, - "StartUpHookSWAIGFunction": { + "PlayWithURL": { "methods": { "__init__": { "params": [ @@ -12782,7 +13431,7 @@ ], "returns": "void" }, - "active": { + "auto_answer": { "params": [ { "name": "self", @@ -12791,7 +13440,7 @@ ], "returns": "any" }, - "argument": { + "url": { "params": [ { "name": "self", @@ -12800,7 +13449,7 @@ ], "returns": "any" }, - "data_map": { + "volume": { "params": [ { "name": "self", @@ -12808,17 +13457,21 @@ } ], "returns": "any" - }, - "fillers": { + } + } + }, + "PlayWithURLS": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "parameters": { + "auto_answer": { "params": [ { "name": "self", @@ -12827,7 +13480,7 @@ ], "returns": "any" }, - "skip_fillers": { + "urls": { "params": [ { "name": "self", @@ -12836,7 +13489,7 @@ ], "returns": "any" }, - "wait_for_fillers": { + "volume": { "params": [ { "name": "self", @@ -12847,7 +13500,7 @@ } } }, - "StopAction": { + "PomSectionBodyContent": { "methods": { "__init__": { "params": [ @@ -12858,7 +13511,7 @@ ], "returns": "void" }, - "stop": { + "numbered": { "params": [ { "name": "self", @@ -12866,21 +13519,17 @@ } ], "returns": "any" - } - } - }, - "StopPlaybackBGAction": { - "methods": { - "__init__": { + }, + "numberedBullets": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "stop_playback_bg": { + "subsections": { "params": [ { "name": "self", @@ -12891,7 +13540,7 @@ } } }, - "StringProperty": { + "PomSectionBulletsContent": { "methods": { "__init__": { "params": [ @@ -12902,7 +13551,7 @@ ], "returns": "void" }, - "format": { + "numbered": { "params": [ { "name": "self", @@ -12911,7 +13560,16 @@ ], "returns": "any" }, - "nullable": { + "numberedBullets": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "subsections": { "params": [ { "name": "self", @@ -12922,7 +13580,7 @@ } } }, - "SummarizeConversationSWAIGFunction": { + "PromptConfig": { "methods": { "__init__": { "params": [ @@ -12933,7 +13591,7 @@ ], "returns": "void" }, - "active": { + "digit_timeout": { "params": [ { "name": "self", @@ -12942,7 +13600,7 @@ ], "returns": "any" }, - "argument": { + "initial_timeout": { "params": [ { "name": "self", @@ -12951,7 +13609,7 @@ ], "returns": "any" }, - "data_map": { + "max_digits": { "params": [ { "name": "self", @@ -12960,7 +13618,7 @@ ], "returns": "any" }, - "fillers": { + "play": { "params": [ { "name": "self", @@ -12969,7 +13627,7 @@ ], "returns": "any" }, - "parameters": { + "speech_end_timeout": { "params": [ { "name": "self", @@ -12978,7 +13636,7 @@ ], "returns": "any" }, - "skip_fillers": { + "speech_hints": { "params": [ { "name": "self", @@ -12987,7 +13645,7 @@ ], "returns": "any" }, - "wait_for_fillers": { + "speech_timeout": { "params": [ { "name": "self", @@ -12998,7 +13656,7 @@ } } }, - "SwitchConfig": { + "Pronounce": { "methods": { "__init__": { "params": [ @@ -13009,7 +13667,7 @@ ], "returns": "void" }, - "default": { + "ignore_case": { "params": [ { "name": "self", @@ -13020,7 +13678,7 @@ } } }, - "TapConfig": { + "RecordCallConfig": { "methods": { "__init__": { "params": [ @@ -13031,7 +13689,7 @@ ], "returns": "void" }, - "rtp_ptime": { + "beep": { "params": [ { "name": "self", @@ -13039,21 +13697,17 @@ } ], "returns": "any" - } - } - }, - "UserSWAIGFunction": { - "methods": { - "__init__": { + }, + "end_silence_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "active": { + "initial_timeout": { "params": [ { "name": "self", @@ -13062,7 +13716,7 @@ ], "returns": "any" }, - "argument": { + "input_sensitivity": { "params": [ { "name": "self", @@ -13071,7 +13725,7 @@ ], "returns": "any" }, - "data_map": { + "max_length": { "params": [ { "name": "self", @@ -13080,7 +13734,7 @@ ], "returns": "any" }, - "fillers": { + "stereo": { "params": [ { "name": "self", @@ -13088,17 +13742,21 @@ } ], "returns": "any" - }, - "parameters": { + } + } + }, + "RecordConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "skip_fillers": { + "beep": { "params": [ { "name": "self", @@ -13107,7 +13765,7 @@ ], "returns": "any" }, - "wait_for_fillers": { + "end_silence_timeout": { "params": [ { "name": "self", @@ -13115,21 +13773,17 @@ } ], "returns": "any" - } - } - }, - "Webhook": { - "methods": { - "__init__": { + }, + "initial_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "expressions": { + "input_sensitivity": { "params": [ { "name": "self", @@ -13138,7 +13792,7 @@ ], "returns": "any" }, - "input_args_as_params": { + "max_length": { "params": [ { "name": "self", @@ -13147,7 +13801,7 @@ ], "returns": "any" }, - "output": { + "stereo": { "params": [ { "name": "self", @@ -13157,12 +13811,8 @@ "returns": "any" } } - } - } - }, - "signalwire.logger": { - "classes": { - "Logger": { + }, + "RequestConfig": { "methods": { "__init__": { "params": [ @@ -13173,370 +13823,254 @@ ], "returns": "void" }, - "debug": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true - } - ], - "returns": "void" - }, - "error": { + "connect_timeout": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "info": { + "save_variables": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" - }, - "instance": { - "params": [], - "returns": "class:signalwire.logger.Logger" + "returns": "any" }, - "is_suppressed": { + "timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" - }, - "level": { + "returns": "any" + } + } + }, + "SWAIG": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.log_level.LogLevel" + "returns": "void" }, - "log": { + "defaults": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "level", - "type": "class:signalwire.log_level.LogLevel", - "required": true - }, - { - "name": "message", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "set_level": { + "functions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "level", - "type": "class:signalwire.log_level.LogLevel", - "required": true } ], - "returns": "void" + "returns": "any" }, - "suppress": { + "includes": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "unsuppress": { + "internal_fillers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "warn": { + "native_functions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" } } - } - } - }, - "signalwire.logging.logger": { - "classes": { - "Logger": { + }, + "SWAIGInternalFiller": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], "returns": "void" }, - "debug": { + "adjust_response_latency": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "error": { + "change_context": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "info": { + "check_time": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "warn": { + "get_ideal_strategy": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "void" - } - } - } - } - }, - "signalwire.pom.pom": { - "classes": { - "PromptObjectModel": { - "methods": { - "__init__": { + "returns": "any" + }, + "get_visual_input": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "add_pom_as_subsection": { + "hangup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "target_title", - "type": "string", - "required": true - }, - { - "name": "pom_to_add", - "type": "class:signalwire.pom.pom.PromptObjectModel", - "required": true } ], - "returns": "void" + "returns": "any" }, - "add_section": { + "next_step": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "title", - "type": "string", - "required": false, - "default": null - }, - { - "name": "body", - "type": "string", - "required": false, - "default": null - }, - { - "name": "bullets", - "type": "list", - "required": false, - "default": null - }, - { - "name": "numbered", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "numbered_bullets", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.pom.pom.Section" + "returns": "any" }, - "find_section": { + "wait_for_user": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "title", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.pom.pom.Section" - }, - "from_json": { - "params": [ - { - "name": "json_text", - "type": "string", - "required": true } ], - "returns": "class:signalwire.pom.pom.PromptObjectModel" + "returns": "any" }, - "from_yaml": { + "wait_seconds": { "params": [ { - "name": "yaml_text", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.pom.pom.PromptObjectModel" - }, - "render_markdown": { + "returns": "any" + } + } + }, + "Section": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "render_xml": { + "main": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "to_dict": { + "returns": "any" + } + } + }, + "SendSMS": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "to_json": { + "send_sms": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" + } + } + }, + "Sleep": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "to_yaml": { + "sleep": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" } } }, - "Section": { + "StartUpHookSWAIGFunction": { "methods": { "__init__": { "params": [ @@ -13547,115 +14081,52 @@ ], "returns": "void" }, - "add_body": { + "active": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "b", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "any" }, - "add_bullets": { + "argument": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "bs", - "type": "list", - "required": true } ], - "returns": "void" + "returns": "any" }, - "add_subsection": { + "data_map": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "title", - "type": "string", - "required": true - }, - { - "name": "body", - "type": "string", - "required": false, - "default": null - }, - { - "name": "bullets", - "type": "list", - "required": false, - "default": null - }, - { - "name": "numbered", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "numbered_bullets", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.pom.pom.Section" + "returns": "any" }, - "render_markdown": { + "fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "level", - "type": "int", - "required": false, - "default": null - }, - { - "name": "section_number", - "type": "list", - "required": false, - "default": null } ], - "returns": "string" + "returns": "any" }, - "render_xml": { + "parameters": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "indent", - "type": "int", - "required": false, - "default": null - }, - { - "name": "section_number", - "type": "list", - "required": false, - "default": null } ], - "returns": "string" + "returns": "any" }, - "to_dict": { + "skip_fillers": { "params": [ { "name": "self", @@ -13664,7 +14135,7 @@ ], "returns": "any" }, - "to_json": { + "wait_for_fillers": { "params": [ { "name": "self", @@ -13674,587 +14145,362 @@ "returns": "any" } } - } - } - }, - "signalwire.prefabs.concierge": { - "classes": { - "ConciergeAgent": { + }, + "StopAction": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": false, - "default": null - }, - { - "name": "route", - "type": "string", - "required": false, - "default": null - }, - { - "name": "host", - "type": "string", - "required": false, - "default": null - }, - { - "name": "port", - "type": "int", - "required": false, - "default": null } ], "returns": "void" }, - "check_availability": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" - }, - "get_directions": { + "returns": "any" + } + } + }, + "StopPlaybackBGAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "on_summary": { + "stop_playback_bg": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:Callable", - "required": true } ], - "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" - }, - "set_amenities": { + "returns": "any" + } + } + }, + "StringProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "amenities", - "type": "list", - "required": true } ], - "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" + "returns": "void" }, - "set_hours": { + "format": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "hours", - "type": "any", - "required": true } ], - "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" + "returns": "any" }, - "set_venue_name": { + "nullable": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" + "returns": "any" } } - } - } - }, - "signalwire.prefabs.faq_bot": { - "classes": { - "FAQBotAgent": { + }, + "SummarizeConversationSWAIGFunction": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "active": { + "params": [ { - "name": "name", - "type": "string", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "argument": { + "params": [ { - "name": "route", - "type": "string", - "required": false, - "default": null - }, - { - "name": "host", - "type": "string", - "required": false, - "default": null - }, - { - "name": "port", - "type": "int", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "on_summary": { + "data_map": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:Callable", - "required": true } ], - "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" + "returns": "any" }, - "search_faqs": { + "fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "set_faqs": { + "parameters": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "faqs", - "type": "list", - "required": true } ], - "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" + "returns": "any" }, - "set_no_match_message": { + "skip_fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" + "returns": "any" }, - "set_suggest_related": { + "wait_for_fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "suggest", - "type": "bool", - "required": true } ], - "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" + "returns": "any" } } - } - } - }, - "signalwire.prefabs.info_gatherer": { - "classes": { - "InfoGathererAgent": { + }, + "SwitchConfig": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": false, - "default": null - }, - { - "name": "route", - "type": "string", - "required": false, - "default": null - }, + } + ], + "returns": "void" + }, + "default": { + "params": [ { - "name": "host", - "type": "string", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "TapConfig": { + "methods": { + "__init__": { + "params": [ { - "name": "port", - "type": "int", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "void" }, - "on_swml_request": { + "rtp_ptime": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_data", - "type": "any", - "required": true - }, + } + ], + "returns": "any" + } + } + }, + "UserSWAIGFunction": { + "methods": { + "__init__": { + "params": [ { - "name": "query_params", - "type": "any", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "active": { + "params": [ { - "name": "headers", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], "returns": "any" }, - "set_completion_message": { + "argument": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" + "returns": "any" }, - "set_prefix": { + "data_map": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "prefix", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" + "returns": "any" }, - "set_question_callback": { + "fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:signalwire.question_callback.QuestionCallback", - "required": true } ], - "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" + "returns": "any" }, - "set_questions": { + "parameters": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "questions", - "type": "list", - "required": true } ], - "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" + "returns": "any" }, - "start_questions": { + "skip_fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" }, - "submit_answer": { + "wait_for_fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "any" } } - } - } - }, - "signalwire.prefabs.receptionist": { - "classes": { - "ReceptionistAgent": { + }, + "Webhook": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": false, - "default": null - }, - { - "name": "route", - "type": "string", - "required": false, - "default": null - }, - { - "name": "host", - "type": "string", - "required": false, - "default": null - }, - { - "name": "port", - "type": "int", - "required": false, - "default": null } ], "returns": "void" }, - "on_summary": { + "expressions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:Callable", - "required": true } ], - "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" + "returns": "any" }, - "set_departments": { + "input_args_as_params": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "departments", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" - }, - "set_greeting": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "greeting", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" + "returns": "any" }, - "set_transfer_message": { + "output": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" + "returns": "any" } } } } }, - "signalwire.prefabs.survey": { + "signalwire.logger": { "classes": { - "SurveyAgent": { + "Logger": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": false, - "default": null - }, - { - "name": "route", - "type": "string", - "required": false, - "default": null - }, - { - "name": "host", - "type": "string", - "required": false, - "default": null - }, - { - "name": "port", - "type": "int", - "required": false, - "default": null } ], "returns": "void" }, - "log_response": { + "debug": { "params": [ { "name": "self", "kind": "self" }, { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", + "name": "msg", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "on_summary": { + "error": { "params": [ { "name": "self", "kind": "self" }, { - "name": "cb", - "type": "class:Callable", + "name": "msg", + "type": "string", "required": true } ], - "returns": "class:signalwire.prefabs.survey.SurveyAgent" + "returns": "void" }, - "set_completion_message": { + "info": { "params": [ { "name": "self", @@ -14266,301 +14512,301 @@ "required": true } ], - "returns": "class:signalwire.prefabs.survey.SurveyAgent" + "returns": "void" }, - "set_intro_message": { + "instance": { + "params": [], + "returns": "class:signalwire.logger.Logger" + }, + "is_suppressed": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "msg", - "type": "string", - "required": true } ], - "returns": "class:signalwire.prefabs.survey.SurveyAgent" + "returns": "bool" }, - "set_questions": { + "level": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "questions", - "type": "list", - "required": true } ], - "returns": "class:signalwire.prefabs.survey.SurveyAgent" + "returns": "class:signalwire.log_level.LogLevel" }, - "validate_response": { + "log": { "params": [ { "name": "self", "kind": "self" }, { - "name": "args", - "type": "any", + "name": "level", + "type": "class:signalwire.log_level.LogLevel", "required": true }, { - "name": "raw_data", - "type": "any", + "name": "message", + "type": "string", "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" - } - } - } - } - }, - "signalwire.relay.action": { - "classes": { - "Action": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], "returns": "void" }, - "call_id": { + "set_level": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "string" - }, - "completed": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "level", + "type": "class:signalwire.log_level.LogLevel", + "required": true } ], - "returns": "bool" + "returns": "void" }, - "control_id": { + "suppress": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "event_type_filter": { + "unsuppress": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "event_type_matches": { + "warn": { "params": [ { "name": "self", "kind": "self" }, { - "name": "event_type", + "name": "msg", "type": "string", "required": true } ], - "returns": "bool" - }, - "is_done": { + "returns": "void" + } + } + } + } + }, + "signalwire.logging.logger": { + "classes": { + "Logger": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "bool" - }, - "method_prefix": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "name", + "type": "string", + "required": true } ], - "returns": "string" + "returns": "void" }, - "node_id": { + "debug": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "msg", + "type": "string", + "required": true } ], - "returns": "string" + "returns": "void" }, - "on_completed": { + "error": { "params": [ { "name": "self", "kind": "self" }, { - "name": "cb", - "type": "callable,void>", + "name": "msg", + "type": "string", "required": true } ], "returns": "void" }, - "pause": { + "info": { "params": [ { "name": "self", "kind": "self" }, { - "name": "behavior", - "type": "optional", - "required": false, - "default": null + "name": "msg", + "type": "string", + "required": true } ], "returns": "void" }, - "resolve": { + "warn": { "params": [ { "name": "self", "kind": "self" }, { - "name": "final_state", + "name": "msg", "type": "string", - "required": false, - "default": null - }, - { - "name": "result", - "type": "any", - "required": false, - "default": null + "required": true } ], "returns": "void" - }, - "resolve_on_detect": { + } + } + } + } + }, + "signalwire.pom.pom": { + "classes": { + "PromptObjectModel": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "void" }, - "resolve_on_result": { + "add_pom_as_subsection": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "target_title", + "type": "string", + "required": true + }, + { + "name": "pom_to_add", + "type": "class:signalwire.pom.pom.PromptObjectModel", + "required": true } ], - "returns": "bool" + "returns": "void" }, - "result": { + "add_section": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - }, - "resume": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "title", + "type": "string", + "required": false, + "default": null + }, + { + "name": "body", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "list", + "required": false, + "default": null + }, + { + "name": "numbered", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "numbered_bullets", + "type": "bool", + "required": false, + "default": false } ], - "returns": "void" + "returns": "class:signalwire.pom.pom.Section" }, - "set_event_type_filter": { + "debug": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "types", - "type": "list", - "required": true } ], - "returns": "void" + "returns": "any" }, - "set_method_prefix": { + "find_section": { "params": [ { "name": "self", "kind": "self" }, { - "name": "prefix", + "name": "title", "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.pom.pom.Section" }, - "set_resolve_on_detect": { + "from_json": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "flag", - "type": "bool", + "name": "json_text", + "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.pom.pom.PromptObjectModel" }, - "set_resolve_on_result": { + "from_yaml": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "flag", - "type": "bool", + "name": "yaml_text", + "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.pom.pom.PromptObjectModel" }, - "start_input_timers": { + "render_markdown": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "state": { + "render_xml": { "params": [ { "name": "self", @@ -14569,73 +14815,47 @@ ], "returns": "string" }, - "stop": { + "sections": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "update_state": { + "to_dict": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "new_state", - "type": "string", - "required": true - }, - { - "name": "result", - "type": "any", - "required": false, - "default": null } ], - "returns": "void" + "returns": "any" }, - "volume": { + "to_json": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "amount", - "type": "float", - "required": true } ], - "returns": "void" + "returns": "string" }, - "wait": { + "to_yaml": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "timeout_ms", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" + "returns": "string" } } - } - } - }, - "signalwire.relay.call": { - "classes": { - "AIAction": { + }, + "Section": { "methods": { - "stop": { + "__init__": { "params": [ { "name": "self", @@ -14643,393 +14863,331 @@ } ], "returns": "void" - } - } - }, - "Call": { - "methods": { - "__init__": { + }, + "add_body": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "b", + "type": "string", + "required": true } ], "returns": "void" }, - "__repr__": { + "add_bullets": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "bs", + "type": "list", + "required": true } ], - "returns": "string" + "returns": "void" }, - "ai": { + "add_subsection": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "title", + "type": "string", "required": true }, { - "name": "control_id", + "name": "body", "type": "string", "required": false, + "default": "" + }, + { + "name": "bullets", + "type": "list", + "required": false, "default": null + }, + { + "name": "numbered", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "numbered_bullets", + "type": "bool", + "required": false, + "default": false } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.pom.pom.Section" }, - "ai_hold": { + "body": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "ai_message": { + "bullets": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "ai_unhold": { + "numbered": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "amazon_bedrock": { + "numberedBullets": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "answer": { + "render_markdown": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "level", + "type": "int", + "required": false, + "default": 2 + }, + { + "name": "section_number", + "type": "list", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "bind_digit": { + "render_xml": { "params": [ { "name": "self", "kind": "self" }, { - "name": "digits", - "type": "string", - "required": true - }, - { - "name": "bind_method", - "type": "string", - "required": true + "name": "indent", + "type": "int", + "required": false, + "default": 0 }, { - "name": "params", - "type": "any", + "name": "section_number", + "type": "list", "required": false, "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "call_id": { + "subsections": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "call_state": { + "title": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "clear_digit_bindings": { + "to_dict": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "realm", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "collect": { + "to_json": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" - }, - "connect": { + "returns": "any" + } + } + } + } + }, + "signalwire.prefabs.concierge": { + "classes": { + "ConciergeAgent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "devices", - "type": "any", - "required": true + "name": "name", + "type": "string", + "required": false, + "default": "concierge" }, { - "name": "options", - "type": "any", + "name": "route", + "type": "string", "required": false, - "default": null - } - ], - "returns": "class:signalwire.relay.action.Action" - }, - "denoise": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "class:signalwire.relay.action.Action" - }, - "denoise_stop": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "class:signalwire.relay.action.Action" - }, - "detect": { - "params": [ - { - "name": "self", - "kind": "self" + "default": "/concierge" }, { - "name": "params", - "type": "any", - "required": true + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" }, { - "name": "control_id", - "type": "string", + "name": "port", + "type": "int", "required": false, - "default": null + "default": 3000 } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "detect_answering_machine": { + "amenities": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "amd_params", - "type": "any", - "required": false, - "default": null - }, - { - "name": "timeout", - "type": "float", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "detect_digit": { + "check_availability": { "params": [ { "name": "self", "kind": "self" }, { - "name": "digits", - "type": "string", - "required": false, - "default": null + "name": "args", + "type": "any", + "required": true }, { - "name": "timeout", - "type": "float", - "required": false, - "default": null + "name": "raw_data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "detect_fax": { + "get_directions": { "params": [ { "name": "self", "kind": "self" }, { - "name": "tone", - "type": "string", - "required": false, - "default": null + "name": "args", + "type": "any", + "required": true }, { - "name": "timeout", - "type": "float", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.relay.action.Action" - }, - "direction": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "raw_data", + "type": "any", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "disconnect": { + "hours_of_operation": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "dispatch_event": { + "on_summary": { "params": [ { "name": "self", "kind": "self" }, { - "name": "ev", - "type": "class:signalwire.relay.call_event.CallEvent", + "name": "cb", + "type": "class:Callable", "required": true } ], - "returns": "void" + "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" }, - "echo": { + "services": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "execute_swml": { + "set_hours": { "params": [ { "name": "self", "kind": "self" }, { - "name": "swml", + "name": "hours", "type": "any", "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.concierge.ConciergeAgent" }, - "find_action": { + "special_instructions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "control_id", - "type": "string", - "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "from": { + "venue_name": { "params": [ { "name": "self", @@ -15037,875 +15195,815 @@ } ], "returns": "string" - }, - "hangup": { + } + } + } + } + }, + "signalwire.prefabs.faq_bot": { + "classes": { + "FAQBotAgent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "reason", + "name": "name", "type": "string", "required": false, - "default": null + "default": "faq_bot" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/faq" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "int", + "required": false, + "default": 3000 } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "hold": { + "faqs": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "is_answered": { + "on_summary": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "cb", + "type": "class:Callable", + "required": true } ], - "returns": "bool" + "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" }, - "is_ended": { + "persona": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "string" }, - "join_conference": { + "search_faqs": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "args", + "type": "any", "required": true }, { - "name": "params", + "name": "raw_data", "type": "any", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "join_room": { + "set_no_match_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "msg", "type": "string", "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.faq_bot.FAQBotAgent" }, - "leave_conference": { + "suggest_related": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "conference_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" - }, - "leave_room": { + "returns": "bool" + } + } + } + } + }, + "signalwire.prefabs.info_gatherer": { + "classes": { + "InfoGathererAgent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": false, + "default": "info_gatherer" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/info_gatherer" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "int", + "required": false, + "default": 3000 } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "live_transcribe": { + "on_swml_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "request_data", "type": "any", "required": false, "default": null - } - ], - "returns": "class:signalwire.relay.action.Action" - }, - "live_translate": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "callback_path", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", + "name": "request", "type": "any", "required": false, "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "node_id": { + "set_completion_message": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "msg", + "type": "string", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" }, - "on": { + "set_prefix": { "params": [ { "name": "self", "kind": "self" }, { - "name": "handler", - "type": "callable,void>", + "name": "prefix", + "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" }, - "on_event": { + "set_question_callback": { "params": [ { "name": "self", "kind": "self" }, { - "name": "handler", - "type": "callable,void>", + "name": "cb", + "type": "class:signalwire.question_callback.QuestionCallback", "required": true } ], - "returns": "void" + "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" }, - "pass_": { + "set_questions": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "questions", + "type": "list", + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.info_gatherer.InfoGathererAgent" }, - "pay": { + "start_questions": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", + "name": "args", "type": "any", "required": true }, { - "name": "control_id", - "type": "string", - "required": false, - "default": null + "name": "raw_data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "play": { + "submit_answer": { "params": [ { "name": "self", "kind": "self" }, { - "name": "media", + "name": "args", "type": "any", "required": true }, { - "name": "volume", - "type": "float", - "required": false, - "default": null - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null + "name": "raw_data", + "type": "any", + "required": true } ], - "returns": "class:signalwire.relay.action.Action" - }, - "play_and_collect": { + "returns": "class:signalwire.core.function_result.FunctionResult" + } + } + } + } + }, + "signalwire.prefabs.receptionist": { + "classes": { + "ReceptionistAgent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "play_media", - "type": "any", - "required": true + "name": "name", + "type": "string", + "required": false, + "default": "receptionist" }, { - "name": "collect_params", - "type": "any", - "required": true + "name": "route", + "type": "string", + "required": false, + "default": "/receptionist" }, { - "name": "control_id", + "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "int", + "required": false, + "default": 3000 } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "play_audio": { + "on_summary": { "params": [ { "name": "self", "kind": "self" }, { - "name": "url", - "type": "string", + "name": "cb", + "type": "class:Callable", "required": true - }, - { - "name": "volume", - "type": "float", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" }, - "play_ringtone": { + "set_departments": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "departments", + "type": "any", "required": true - }, - { - "name": "duration", - "type": "float", - "required": false, - "default": null - }, - { - "name": "volume", - "type": "float", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" }, - "play_silence": { + "set_greeting": { "params": [ { "name": "self", "kind": "self" }, { - "name": "duration", - "type": "float", + "name": "greeting", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" }, - "play_tts": { + "set_transfer_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", + "name": "msg", "type": "string", "required": true + } + ], + "returns": "class:signalwire.prefabs.receptionist.ReceptionistAgent" + } + } + } + } + }, + "signalwire.prefabs.survey": { + "classes": { + "SurveyAgent": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "language", + "name": "name", "type": "string", "required": false, - "default": null + "default": "survey" }, { - "name": "gender", + "name": "route", "type": "string", "required": false, - "default": null + "default": "/survey" }, { - "name": "voice", + "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { - "name": "volume", - "type": "float", + "name": "port", + "type": "int", "required": false, - "default": null + "default": 3000 } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "prompt": { + "brand_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "play_media", - "type": "any", - "required": true - }, - { - "name": "collect_params", - "type": "any", - "required": true - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "prompt_audio": { + "conclusion": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "collect", - "type": "any", - "required": true - }, + } + ], + "returns": "string" + }, + "introduction": { + "params": [ { - "name": "volume", - "type": "float", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "prompt_tts": { + "log_response": { "params": [ { "name": "self", "kind": "self" }, { - "name": "text", - "type": "string", + "name": "args", + "type": "any", "required": true }, { - "name": "collect", + "name": "raw_data", "type": "any", "required": true - }, - { - "name": "language", - "type": "string", - "required": false, - "default": null - }, - { - "name": "gender", - "type": "string", - "required": false, - "default": null - }, - { - "name": "voice", - "type": "string", - "required": false, - "default": null - }, - { - "name": "volume", - "type": "float", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.core.function_result.FunctionResult" }, - "queue_enter": { + "max_retries": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "queue_name", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "int" }, - "queue_leave": { + "on_summary": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_name", - "type": "string", + "name": "cb", + "type": "class:Callable", "required": true - }, + } + ], + "returns": "class:signalwire.prefabs.survey.SurveyAgent" + }, + "questions": { + "params": [ { - "name": "params", - "type": "any", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "receive_fax": { + "set_completion_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "control_id", + "name": "msg", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.survey.SurveyAgent" }, - "record": { + "set_intro_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null - }, - { - "name": "control_id", + "name": "msg", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "class:signalwire.prefabs.survey.SurveyAgent" }, - "record_call": { + "survey_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "refer": { + "validate_response": { "params": [ { "name": "self", "kind": "self" }, { - "name": "device", + "name": "args", "type": "any", "required": true }, { - "name": "params", + "name": "raw_data", "type": "any", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.relay.action.Action" - }, - "register_action": { + "returns": "class:signalwire.core.function_result.FunctionResult" + } + } + } + } + }, + "signalwire.relay.action": { + "classes": { + "Action": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "control_id", - "type": "string", - "required": true - }, - { - "name": "action", - "type": "class:signalwire.relay.action.Action", - "required": true } ], "returns": "void" }, - "resolve_all_actions": { + "call_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "completed": { + "params": [ { - "name": "final_state", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "void" + "returns": "bool" }, - "send_digits": { + "control_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "event_type_filter": { + "params": [ { - "name": "digits", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "list" }, - "send_fax": { + "event_type_matches": { "params": [ { "name": "self", "kind": "self" }, { - "name": "document_url", + "name": "event_type", "type": "string", "required": true - }, - { - "name": "header", - "type": "string", - "required": false, - "default": null - }, - { - "name": "identity", - "type": "string", - "required": false, - "default": null - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "bool" }, - "set_client": { + "is_done": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "c", - "type": "class:signalwire.relay.client.RelayClient", - "required": true } ], - "returns": "void" + "returns": "bool" }, - "set_direction": { + "method_prefix": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "dir", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "string" }, - "set_from": { + "node_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "f", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "string" }, - "set_tag": { + "on_completed": { "params": [ { "name": "self", "kind": "self" }, { - "name": "t", - "type": "string", + "name": "cb", + "type": "callable,void>", "required": true } ], "returns": "void" }, - "set_to": { + "pause": { "params": [ { "name": "self", "kind": "self" }, { - "name": "t", - "type": "string", - "required": true + "name": "behavior", + "type": "optional", + "required": false, + "default": null } ], "returns": "void" }, - "sip_refer": { + "resolve": { "params": [ { "name": "self", "kind": "self" }, { - "name": "to_uri", + "name": "final_state", "type": "string", - "required": true + "required": false, + "default": "finished" + }, + { + "name": "result", + "type": "any", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "state": { + "resolve_on_detect": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "bool" }, - "stop_tap": { + "resolve_on_result": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "control_id", - "type": "string", - "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "bool" }, - "stream": { + "result": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "any" }, - "tag": { + "resume": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "tap": { + "set_event_type_filter": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", + "name": "types", + "type": "list", "required": true - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "tap_audio": { + "set_method_prefix": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "control_id", + "name": "prefix", "type": "string", - "required": false, - "default": null + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "to": { + "set_resolve_on_detect": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "flag", + "type": "bool", + "required": true } ], - "returns": "string" + "returns": "void" }, - "transcribe": { + "set_resolve_on_result": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "type": "any", - "required": false, - "default": null - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null + "name": "flag", + "type": "bool", + "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "transfer": { + "start_input_timers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "unhold": { + "state": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "string" }, - "unregister_action": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "control_id", - "type": "string", - "required": true } ], "returns": "void" @@ -15920,46 +16018,31 @@ "name": "new_state", "type": "string", "required": true - } - ], - "returns": "void" - }, - "user_event": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "event", - "type": "string", + "name": "result", + "type": "any", "required": false, "default": null } ], - "returns": "class:signalwire.relay.action.Action" + "returns": "void" }, - "wait_for": { + "volume": { "params": [ { "name": "self", "kind": "self" }, { - "name": "target_state", - "type": "string", + "name": "amount", + "type": "float", "required": true - }, - { - "name": "timeout_ms", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" + "returns": "void" }, - "wait_for_answered": { + "wait": { "params": [ { "name": "self", @@ -15973,117 +16056,102 @@ } ], "returns": "bool" - }, - "wait_for_ended": { + } + } + } + } + }, + "signalwire.relay.call": { + "classes": { + "AIAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "timeout_ms", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" - }, - "wait_for_ending": { + "returns": "void" + } + } + }, + "Action": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "timeout_ms", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" + "returns": "void" }, - "wait_for_ringing": { + "call": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "timeout_ms", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" - } - } - }, - "CollectAction": { - "methods": { - "pause": { + "returns": "class:signalwire.relay.call.Call" + }, + "completed": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "behavior", - "type": "optional", - "required": false, - "default": null } ], - "returns": "void" + "returns": "bool" }, - "resume": { + "control_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "start_input_timers": { + "is_done": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "bool" }, - "stop": { + "result": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "volume": { + "wait": { "params": [ { "name": "self", "kind": "self" }, { - "name": "amount", - "type": "float", - "required": true + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], - "returns": "void" + "returns": "bool" } } }, - "DetectAction": { + "Call": { "methods": { - "stop": { + "__init__": { "params": [ { "name": "self", @@ -16091,413 +16159,525 @@ } ], "returns": "void" - } - } - }, - "FaxAction": { - "methods": { - "stop": { + }, + "__repr__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" - } - } - }, - "PayAction": { - "methods": { - "stop": { + "returns": "string" + }, + "ai": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - } - } - }, - "PlayAction": { - "methods": { - "pause": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "params", + "type": "any", + "required": true }, { - "name": "behavior", - "type": "optional", + "name": "control_id", + "type": "string", "required": false, "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "resume": { + "ai_hold": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "stop": { + "ai_message": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "message_text", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "role", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "reset", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "global_data", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "volume": { + "ai_unhold": { "params": [ { "name": "self", "kind": "self" }, { - "name": "amount", - "type": "float", - "required": true + "name": "prompt", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" - } - } - }, - "RecordAction": { - "methods": { - "pause": { + "returns": "class:signalwire.relay.action.Action" + }, + "amazon_bedrock": { "params": [ { "name": "self", "kind": "self" }, { - "name": "behavior", + "name": "prompt", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "SWAIG", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "ai_params", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "global_data", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "post_prompt", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "kind": "keyword", "type": "optional", "required": false, "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "resume": { + "answer": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "stop": { + "bind_digit": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "digits", + "type": "string", + "required": true + }, + { + "name": "bind_method", + "type": "string", + "required": true + }, + { + "name": "bind_params", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "realm", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "max_triggers", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" - } - } - }, - "StandaloneCollectAction": { - "methods": { - "start_input_timers": { + "returns": "class:signalwire.relay.action.Action" + }, + "call_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "stop": { + "call_state": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" - } - } - }, - "StreamAction": { - "methods": { - "stop": { + "returns": "optional" + }, + "clear_digit_bindings": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "realm", + "type": "string", + "required": false, + "default": null } ], - "returns": "void" - } - } - }, - "TapAction": { - "methods": { - "stop": { + "returns": "class:signalwire.relay.action.Action" + }, + "collect": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "void" - } - } - }, - "TranscribeAction": { - "methods": { - "stop": { + "returns": "class:signalwire.relay.action.Action" + }, + "connect": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "devices", + "type": "any", + "required": true + }, + { + "name": "options", + "type": "any", + "required": false, + "default": null } ], - "returns": "void" - } - } - } - } - }, - "signalwire.relay.call_event": { - "classes": { - "CallEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "context": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_relay_event": { - "params": [ - { - "name": "ev", - "type": "class:signalwire.relay.relay_event.RelayEvent", - "required": true - } - ], - "returns": "class:signalwire.relay.call_event.CallEvent" - } - } - } - } - }, - "signalwire.relay.client": { - "classes": { - "RelayClient": { - "methods": { - "__init__": { + "denoise": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "config", - "type": "class:signalwire.relay.relay_config.RelayConfig", - "required": false, - "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "config": { + "denoise_stop": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.relay.relay_config.RelayConfig" + "returns": "class:signalwire.relay.action.Action" }, - "connect": { + "detect": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "bool" + "returns": "class:signalwire.relay.action.Action" }, - "dial": { + "detect_answering_machine": { "params": [ { "name": "self", "kind": "self" }, { - "name": "devices", - "type": "any", - "required": true + "name": "initial_timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "tag", - "type": "string", + "name": "end_silence_timeout", + "kind": "keyword", + "type": "optional", "required": false, "default": null }, { - "name": "dial_timeout_ms", - "type": "int", + "name": "machine_voice_threshold", + "kind": "keyword", + "type": "optional", "required": false, "default": null }, { - "name": "max_duration", - "type": "int", + "name": "machine_words_threshold", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "detect_interruptions", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "detect_message_end", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "timeout", + "kind": "keyword", + "type": "float", "required": false, "default": null } ], - "returns": "class:signalwire.relay.call.Call" + "returns": "class:signalwire.relay.action.Action" }, - "disconnect": { + "detect_digit": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "digits", + "type": "string", + "required": false, + "default": null + }, + { + "name": "timeout", + "type": "float", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "execute": { + "detect_fax": { "params": [ { "name": "self", "kind": "self" }, { - "name": "method", + "name": "tone", "type": "string", - "required": true + "required": false, + "default": null }, { - "name": "params", - "type": "any", - "required": true + "name": "timeout", + "type": "float", + "required": false, + "default": null } ], - "returns": "any" + "returns": "class:signalwire.relay.action.Action" }, - "find_call": { + "device": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "class:signalwire.relay.call.Call" - }, - "from_env": { - "params": [], - "returns": "class:signalwire.relay.client.RelayClient" + "returns": "any" }, - "is_connected": { + "direction": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "string" }, - "on_call": { + "disconnect": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "handler", - "type": "class:signalwire.relay.client.CallHandler", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "on_event": { + "dispatch_event": { "params": [ { "name": "self", "kind": "self" }, { - "name": "handler", - "type": "callable,void>", + "name": "ev", + "type": "class:signalwire.relay.call_event.CallEvent", "required": true } ], "returns": "void" }, - "on_message": { + "echo": { "params": [ { "name": "self", "kind": "self" }, { - "name": "handler", - "type": "class:signalwire.relay.client.MessageHandler", - "required": true + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "receive": { + "execute_swml": { "params": [ { "name": "self", "kind": "self" }, { - "name": "contexts", - "type": "list", + "name": "swml", + "type": "any", "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "register_call": { + "find_action": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", + "name": "control_id", "type": "string", "required": true - }, - { - "name": "call", - "type": "class:signalwire.relay.call.Call", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "relay_protocol": { + "from": { "params": [ { "name": "self", @@ -16506,180 +16686,140 @@ ], "returns": "string" }, - "run": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" - }, - "send_message": { + "hangup": { "params": [ { "name": "self", "kind": "self" }, { - "name": "from", - "type": "string", - "required": true - }, - { - "name": "to", - "type": "string", - "required": true - }, - { - "name": "body", - "type": "string", - "required": true - }, - { - "name": "media", - "type": "list", - "required": false, - "default": null - }, - { - "name": "tags", - "type": "list", - "required": false, - "default": null - }, - { - "name": "region", - "type": "string", - "required": false, - "default": null - }, - { - "name": "context", + "name": "reason", "type": "string", "required": false, - "default": null + "default": "hangup" } ], - "returns": "class:signalwire.relay.message.Message" + "returns": "class:signalwire.relay.action.Action" }, - "send_raw_request": { + "hold": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.action.Action" }, - "session_id": { + "is_answered": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "bool" }, - "subscribe": { + "is_ended": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "contexts", - "type": "list", - "required": true } ], - "returns": "void" + "returns": "bool" }, - "unreceive": { + "join_conference": { "params": [ { "name": "self", "kind": "self" }, { - "name": "contexts", - "type": "list", + "name": "name", + "type": "string", "required": true + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "unregister_call": { + "join_room": { "params": [ { "name": "self", "kind": "self" }, { - "name": "call_id", + "name": "name", "type": "string", "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "unsubscribe": { + "leave_conference": { "params": [ { "name": "self", "kind": "self" }, { - "name": "contexts", - "type": "list", + "name": "conference_id", + "type": "string", "required": true } ], - "returns": "void" - } - } - }, - "RelayError": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "leave_room": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "class:signalwire.relay.action.Action" + }, + "live_transcribe": { + "params": [ { - "name": "code", - "type": "int", - "required": true + "name": "self", + "kind": "self" }, { - "name": "message", - "type": "string", + "name": "action", + "type": "any", "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "code": { + "live_translate": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "action", + "type": "any", + "required": true + }, + { + "name": "status_url", + "type": "string", + "required": false, + "default": null } ], - "returns": "int" + "returns": "class:signalwire.relay.action.Action" }, - "message": { + "node_id": { "params": [ { "name": "self", @@ -16687,854 +16827,959 @@ } ], "returns": "string" - } - } - } - } - }, - "signalwire.relay.component_event": { - "classes": { - "ComponentEvent": { - "methods": { - "__init__": { + }, + "on": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "handler", + "type": "callable,void>", + "required": true } ], "returns": "void" }, - "from_relay_event": { + "on_event": { "params": [ { - "name": "ev", - "type": "class:signalwire.relay.relay_event.RelayEvent", + "name": "self", + "kind": "self" + }, + { + "name": "handler", + "type": "callable,void>", "required": true } ], - "returns": "class:signalwire.relay.component_event.ComponentEvent" - } - } - } - } - }, - "signalwire.relay.device": { - "classes": { - "Device": { - "methods": { - "__init__": { + "returns": "void" + }, + "pass_": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "to_json": { + "pay": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "any" - } - } - } - } - }, - "signalwire.relay.dial_event": { - "classes": { - "DialEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "play": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "media", + "type": "any", + "required": true + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "dial_state_enum": { + "play_and_collect": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "play_media", + "type": "any", + "required": true + }, + { + "name": "collect_params", + "type": "any", + "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "optional" + "returns": "class:signalwire.relay.action.Action" }, - "from_relay_event": { + "play_audio": { "params": [ { - "name": "ev", - "type": "class:signalwire.relay.relay_event.RelayEvent", + "name": "self", + "kind": "self" + }, + { + "name": "url", + "type": "string", "required": true + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.dial_event.DialEvent" - } - } - } - } - }, - "signalwire.relay.event": { - "classes": { - "CallReceiveEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "play_ringtone": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "duration", + "type": "float", + "required": false, + "default": null + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { + "play_silence": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "duration", + "type": "float", "required": true } ], - "returns": "class:signalwire.relay.event.CallReceiveEvent" - } - } - }, - "CallStateEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "play_tts": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", - "type": "any", + "name": "text", + "type": "string", "required": true + }, + { + "name": "language", + "type": "string", + "required": false, + "default": null + }, + { + "name": "gender", + "type": "string", + "required": false, + "default": null + }, + { + "name": "voice", + "type": "string", + "required": false, + "default": null + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.CallStateEvent" - } - } - }, - "CallingErrorEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "project_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_payload": { + "prompt_audio": { "params": [ { - "name": "payload", + "name": "self", + "kind": "self" + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "collect", "type": "any", "required": true + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.CallingErrorEvent" - } - } - }, - "CollectEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "prompt_tts": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", + "name": "text", + "type": "string", + "required": true + }, + { + "name": "collect", "type": "any", "required": true + }, + { + "name": "language", + "type": "string", + "required": false, + "default": null + }, + { + "name": "gender", + "type": "string", + "required": false, + "default": null + }, + { + "name": "voice", + "type": "string", + "required": false, + "default": null + }, + { + "name": "volume", + "type": "float", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.CollectEvent" - } - } - }, - "ConferenceEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "queue_enter": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "queue_name", + "type": "string", + "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { + "queue_leave": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "queue_name", + "type": "string", "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "queue_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.ConferenceEvent" - } - } - }, - "ConnectEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "receive_fax": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { + "record": { "params": [ { - "name": "payload", + "name": "self", + "kind": "self" + }, + { + "name": "params", "type": "any", - "required": true + "required": false, + "default": null + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.ConnectEvent" - } - } - }, - "DenoiseEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "record_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { + "refer": { "params": [ { - "name": "payload", + "name": "self", + "kind": "self" + }, + { + "name": "device", "type": "any", "required": true + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.DenoiseEvent" - } - } - }, - "DetectEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "register_action": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", - "type": "any", + "name": "control_id", + "type": "string", + "required": true + }, + { + "name": "action", + "type": "class:signalwire.relay.action.Action", "required": true } ], - "returns": "class:signalwire.relay.event.DetectEvent" - } - } - }, - "DialEvent": { - "methods": { - "__init__": { + "returns": "void" + }, + "resolve_all_actions": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "final_state", + "type": "string", + "required": false, + "default": "finished" } ], "returns": "void" }, - "from_payload": { - "params": [ - { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.DialEvent" - } - } - }, - "EchoEvent": { - "methods": { - "__init__": { + "segment_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_payload": { + "send_digits": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "digits", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.event.EchoEvent" - } - } - }, - "FaxEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "send_fax": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", - "type": "any", + "name": "document_url", + "type": "string", "required": true + }, + { + "name": "header", + "type": "string", + "required": false, + "default": null + }, + { + "name": "identity", + "type": "string", + "required": false, + "default": null + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.FaxEvent" - } - } - }, - "HoldEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "set_client": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "c", + "type": "class:signalwire.relay.client.RelayClient", + "required": true } ], "returns": "void" }, - "from_payload": { + "set_from": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "f", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.event.HoldEvent" - } - } - }, - "MessageReceiveEvent": { - "methods": { - "__init__": { + "returns": "void" + }, + "set_to": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "t", + "type": "string", + "required": true } ], "returns": "void" }, - "from_payload": { + "sip_refer": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "to_uri", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.event.MessageReceiveEvent" - } - } - }, - "MessageStateEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "state": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_payload": { + "stop_tap": { "params": [ { - "name": "payload", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "control_id", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.event.MessageStateEvent" - } - } - }, - "PayEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "stream": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", + "name": "params", "type": "any", "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.PayEvent" - } - } - }, - "PlayEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "tag": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_payload": { + "tap": { "params": [ { - "name": "payload", + "name": "self", + "kind": "self" + }, + { + "name": "params", "type": "any", "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.event.PlayEvent" - } - } - }, - "QueueEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "tap_audio": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", + "name": "params", "type": "any", "required": true + }, + { + "name": "control_id", + "type": "string", + "required": false, + "default": "" } ], - "returns": "class:signalwire.relay.event.QueueEvent" - } - } - }, - "RecordEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "to": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "string" }, - "from_payload": { - "params": [ - { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.RecordEvent" - } - } - }, - "ReferEvent": { - "methods": { - "__init__": { + "transcribe": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.ReferEvent" - } - } - }, - "RelayEvent": { - "methods": { - "__init__": { - "params": [ + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { - "params": [ - { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.RelayEvent" - } - } - }, - "SendDigitsEvent": { - "methods": { - "__init__": { + "transfer": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", + "name": "params", "type": "any", "required": true } ], - "returns": "class:signalwire.relay.event.SendDigitsEvent" - } - } - }, - "StreamEvent": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.action.Action" + }, + "unhold": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "class:signalwire.relay.action.Action" }, - "from_payload": { - "params": [ - { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.StreamEvent" - } - } - }, - "TapEvent": { - "methods": { - "__init__": { + "unregister_action": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "void" - }, - "from_payload": { - "params": [ + }, { - "name": "payload", - "type": "any", + "name": "control_id", + "type": "string", "required": true } ], - "returns": "class:signalwire.relay.event.TapEvent" - } - } - }, - "TranscribeEvent": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], "returns": "void" }, - "from_payload": { - "params": [ - { - "name": "payload", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.relay.event.TranscribeEvent" - } - } - } - } - }, - "signalwire.relay.message": { - "classes": { - "Message": { - "methods": { - "__init__": { + "update_state": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "new_state", + "type": "string", + "required": true } ], "returns": "void" }, - "__repr__": { + "user_event": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "string" - }, - "from_params": { - "params": [ + }, { - "name": "params", - "type": "any", - "required": true + "name": "event", + "type": "string", + "required": false, + "default": null } ], - "returns": "class:signalwire.relay.message.Message" + "returns": "class:signalwire.relay.action.Action" }, - "is_delivered": { + "wait_for": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "target_state", + "type": "string", + "required": true + }, + { + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], "returns": "bool" }, - "is_done": { + "wait_for_answered": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], "returns": "bool" }, - "is_failed": { + "wait_for_ended": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], "returns": "bool" }, - "is_terminal": { + "wait_for_ending": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], "returns": "bool" }, - "message_state": { + "wait_for_ringing": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], - "returns": "optional" - }, - "on": { + "returns": "bool" + } + } + }, + "CollectAction": { + "methods": { + "pause": { "params": [ { "name": "self", "kind": "self" }, { - "name": "cb", - "type": "callable,void>", - "required": true + "name": "behavior", + "type": "optional", + "required": false, + "default": null } ], "returns": "void" }, - "on_completed": { + "resume": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "callable,void>", - "required": true } ], "returns": "void" }, - "reason": { + "start_input_timers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "result": { + "stop": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "set_reason": { + "volume": { "params": [ { "name": "self", "kind": "self" }, { - "name": "r", - "type": "string", + "name": "amount", + "type": "float", "required": true } ], "returns": "void" - }, - "set_state": { - "params": [ - { - "name": "self", - "kind": "self" - }, + } + } + }, + "DetectAction": { + "methods": { + "stop": { + "params": [ { - "name": "s", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], "returns": "void" - }, - "state": { + } + } + }, + "FaxAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "update_state": { + "returns": "void" + } + } + }, + "PayAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "new_state", - "type": "string", - "required": true } ], "returns": "void" - }, - "wait": { + } + } + }, + "PlayAction": { + "methods": { + "pause": { "params": [ { "name": "self", "kind": "self" }, { - "name": "timeout_ms", - "type": "int", + "name": "behavior", + "type": "optional", "required": false, "default": null } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.relay.message_event": { - "classes": { - "MessageEvent": { - "methods": { - "__init__": { + "returns": "void" + }, + "resume": { "params": [ { "name": "self", @@ -17543,25 +17788,7 @@ ], "returns": "void" }, - "from_relay_event": { - "params": [ - { - "name": "ev", - "type": "class:signalwire.relay.relay_event.RelayEvent", - "required": true - } - ], - "returns": "class:signalwire.relay.message_event.MessageEvent" - } - } - } - } - }, - "signalwire.relay.relay_event": { - "classes": { - "RelayEvent": { - "methods": { - "__init__": { + "stop": { "params": [ { "name": "self", @@ -17570,197 +17797,152 @@ ], "returns": "void" }, - "from_json": { + "volume": { "params": [ { - "name": "j", - "type": "any", + "name": "self", + "kind": "self" + }, + { + "name": "amount", + "type": "float", "required": true } ], - "returns": "class:signalwire.relay.relay_event.RelayEvent" + "returns": "void" } } - } - } - }, - "signalwire.relay.web_socket_client": { - "classes": { - "WebSocketClient": { + }, + "RecordAction": { "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" - }, - "close": { + "pause": { "params": [ { "name": "self", "kind": "self" }, { - "name": "code", - "type": "int", - "required": false, - "default": null - }, - { - "name": "reason", - "type": "string", + "name": "behavior", + "type": "optional", "required": false, "default": null } ], "returns": "void" }, - "connect": { + "resume": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "host", - "type": "string", - "required": true - }, - { - "name": "port", - "type": "int", - "required": false, - "default": null } ], - "returns": "bool" + "returns": "void" }, - "connect_plain": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "host", - "type": "string", - "required": true - }, - { - "name": "port", - "type": "int", - "required": true } ], - "returns": "bool" - }, - "is_connected": { + "returns": "void" + } + } + }, + "StandaloneCollectAction": { + "methods": { + "start_input_timers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "void" }, - "on_close": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:signalwire.close_callback.CloseCallback", - "required": true } ], "returns": "void" - }, - "on_error": { + } + } + }, + "StreamAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:signalwire.error_callback.ErrorCallback", - "required": true } ], "returns": "void" - }, - "on_message": { + } + } + }, + "TapAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "cb", - "type": "class:signalwire.message_callback.MessageCallback", - "required": true } ], "returns": "void" - }, - "send": { + } + } + }, + "TranscribeAction": { + "methods": { + "stop": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "message", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "void" } } } } }, - "signalwire.rest._base": { + "signalwire.relay.call_event": { "classes": { - "BaseResource": { + "CallEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - }, - { - "name": "base_path", - "type": "string", - "required": true } ], "returns": "void" }, - "base_path": { + "from_relay_event": { "params": [ { - "name": "self", - "kind": "self" + "name": "ev", + "type": "class:signalwire.relay.relay_event.RelayEvent", + "required": true } ], - "returns": "string" + "returns": "class:signalwire.relay.call_event.CallEvent" } } - }, - "CrudResource": { + } + } + }, + "signalwire.relay.client": { + "classes": { + "RelayClient": { "methods": { "__init__": { "params": [ @@ -17769,184 +17951,138 @@ "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - }, - { - "name": "base_path", - "type": "string", - "required": true - }, - { - "name": "update_method", - "type": "string", + "name": "config", + "type": "class:signalwire.relay.relay_config.RelayConfig", "required": false, "default": null } ], "returns": "void" }, - "create": { + "config": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "data", - "type": "any", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.relay_config.RelayConfig" }, - "delete": { + "connect": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "bool" + }, + "contexts": { + "params": [ { - "name": "id", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "list" }, - "get": { + "dial": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "type": "string", + "name": "devices", + "type": "any", "required": true }, { - "name": "params", - "type": "dict", + "name": "tag", + "type": "string", + "required": false, + "default": null + }, + { + "name": "max_duration", + "type": "int", + "required": false, + "default": null + }, + { + "name": "dial_timeout", + "type": "optional", "required": false, "default": null } ], - "returns": "any" + "returns": "class:signalwire.relay.call.Call" }, - "list": { + "disconnect": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "dict", - "required": false, - "default": null } ], - "returns": "any" + "returns": "void" }, - "update": { + "execute": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "method", "type": "string", "required": true }, { - "name": "data", + "name": "params", "type": "any", "required": true } ], "returns": "any" - } - } - }, - "CrudWithAddresses": { - "methods": { - "__init__": { + }, + "find_call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - }, - { - "name": "base_path", + "name": "call_id", "type": "string", "required": true - }, - { - "name": "update_method", - "type": "string", - "required": false, - "default": null } ], - "returns": "void" + "returns": "class:signalwire.relay.call.Call" }, - "list_addresses": { + "from_env": { + "params": [], + "returns": "class:signalwire.relay.client.RelayClient" + }, + "host": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "dict", - "required": false, - "default": null } ], - "returns": "any" - } - } - }, - "HttpClient": { - "methods": { - "__init__": { + "returns": "string" + }, + "is_connected": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "base_url", - "type": "string", - "required": true - }, - { - "name": "username", - "type": "string", - "required": true - }, - { - "name": "password", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "bool" }, - "base_url": { + "jwt_token": { "params": [ { "name": "self", @@ -17955,262 +18091,199 @@ ], "returns": "string" }, - "delete": { + "on_call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", - "type": "string", + "name": "handler", + "type": "class:signalwire.relay.client.CallHandler", "required": true } ], - "returns": "any" + "returns": "void" }, - "get": { + "on_event": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", - "type": "string", + "name": "handler", + "type": "callable,void>", "required": true - }, - { - "name": "params", - "type": "dict", - "required": false, - "default": null } ], - "returns": "any" + "returns": "void" }, - "patch": { + "on_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", - "type": "string", + "name": "handler", + "type": "class:signalwire.relay.client.MessageHandler", "required": true - }, - { - "name": "body", - "type": "any", - "required": false, - "default": null } ], - "returns": "any" + "returns": "void" }, - "post": { + "project": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "path", - "type": "string", - "required": true - }, - { - "name": "body", - "type": "any", - "required": false, - "default": null } ], - "returns": "any" + "returns": "string" }, - "put": { + "receive": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", - "type": "string", + "name": "contexts", + "type": "list", "required": true - }, - { - "name": "body", - "type": "any", - "required": false, - "default": null } ], - "returns": "any" + "returns": "void" }, - "set_ca_cert_path": { + "register_call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", + "name": "call_id", "type": "string", "required": true + }, + { + "name": "call", + "type": "class:signalwire.relay.call.Call", + "required": true } ], "returns": "void" }, - "set_header": { + "relay_protocol": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "value", - "type": "string", - "required": true } ], - "returns": "void" + "returns": "string" }, - "set_timeout": { + "run": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "seconds", - "type": "int", - "required": true } ], "returns": "void" - } - } - }, - "ReadResource": { - "methods": { - "__init__": { + }, + "send_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "from", + "type": "string", "required": true }, { - "name": "base_path", + "name": "to", "type": "string", "required": true - } - ], - "returns": "void" - }, - "get": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "id", + "name": "body", "type": "string", "required": true }, { - "name": "params", - "type": "dict", + "name": "media", + "type": "list", "required": false, "default": null - } - ], - "returns": "any" - }, - "list": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "params", - "type": "dict", + "name": "tags", + "type": "list", "required": false, "default": null - } - ], - "returns": "any" - }, - "paginate": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "region", + "type": "string", + "required": false, + "default": null }, { - "name": "params", - "type": "dict", + "name": "context", + "type": "string", "required": false, "default": null } ], - "returns": "class:signalwire.rest._pagination.PaginatedIterator" - } - } - }, - "SignalWireRestError": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.message.Message" + }, + "send_raw_request": { "params": [ { "name": "self", "kind": "self" }, { - "name": "status", - "type": "int", + "name": "method", + "type": "string", "required": true }, { - "name": "message", - "type": "string", + "name": "params", + "type": "any", "required": true - }, + } + ], + "returns": "any" + }, + "session_id": { + "params": [ { - "name": "body", - "type": "string", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "subscribe": { + "params": [ { - "name": "url", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" }, { - "name": "method", - "type": "string", - "required": false, - "default": null + "name": "contexts", + "type": "list", + "required": true } ], "returns": "void" }, - "body": { + "token": { "params": [ { "name": "self", @@ -18219,40 +18292,51 @@ ], "returns": "string" }, - "method": { + "unreceive": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "contexts", + "type": "list", + "required": true } ], - "returns": "string" + "returns": "void" }, - "status": { + "unregister_call": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true } ], - "returns": "int" + "returns": "void" }, - "url": { + "unsubscribe": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "contexts", + "type": "list", + "required": true } ], - "returns": "string" + "returns": "void" } } - } - } - }, - "signalwire.rest._pagination": { - "classes": { - "PaginatedIterator": { + }, + "RelayError": { "methods": { "__init__": { "params": [ @@ -18261,76 +18345,81 @@ "kind": "self" }, { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", + "name": "code", + "type": "int", "required": true }, { - "name": "path", + "name": "message", "type": "string", "required": true - }, - { - "name": "params", - "type": "dict", - "required": false, - "default": null - }, - { - "name": "data_key", - "type": "string", - "required": false, - "default": null } ], "returns": "void" }, - "data_key": { + "code": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "int" }, - "done": { + "message": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" - }, - "has_next": { + "returns": "string" + } + } + } + } + }, + "signalwire.relay.component_event": { + "classes": { + "ComponentEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "void" }, - "http": { + "from_relay_event": { "params": [ { - "name": "self", - "kind": "self" + "name": "ev", + "type": "class:signalwire.relay.relay_event.RelayEvent", + "required": true } ], - "returns": "class:signalwire.rest._base.HttpClient" - }, - "items": { + "returns": "class:signalwire.relay.component_event.ComponentEvent" + } + } + } + } + }, + "signalwire.relay.device": { + "classes": { + "Device": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "next": { + "to_json": { "params": [ { "name": "self", @@ -18338,324 +18427,293 @@ } ], "returns": "any" - }, - "params": { + } + } + } + } + }, + "signalwire.relay.dial_event": { + "classes": { + "DialEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "dict" + "returns": "void" }, - "path": { + "dial_state_enum": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "optional" + }, + "from_relay_event": { + "params": [ + { + "name": "ev", + "type": "class:signalwire.relay.relay_event.RelayEvent", + "required": true + } + ], + "returns": "class:signalwire.relay.dial_event.DialEvent" } } } } }, - "signalwire.rest.client": { + "signalwire.relay.event": { "classes": { - "RestClient": { + "CallReceiveEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "space", - "type": "string", - "required": true - }, - { - "name": "project_id", - "type": "string", - "required": true - }, - { - "name": "token", - "type": "string", - "required": true } ], "returns": "void" }, - "addresses": { + "call_state": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Addresses" + "returns": "any" }, - "calling": { + "context": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.calling_resources_generated.Calling" + "returns": "any" }, - "chat": { + "device": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.chat_resources_generated.Chat" + "returns": "any" }, - "datasphere": { + "direction": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.DatasphereNamespace" + "returns": "any" }, - "fabric": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.FabricNamespace" - }, - "from_env": { - "params": [], - "returns": "class:signalwire.rest.client.RestClient" + "returns": "class:signalwire.relay.event.CallReceiveEvent" }, - "http_client": { + "node_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest._base.HttpClient" + "returns": "any" }, - "imported_numbers": { + "project_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.ImportedNumbers" + "returns": "any" }, - "logs": { + "segment_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.LogsNamespace" + "returns": "any" }, - "lookup": { + "tag": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Lookup" - }, - "mfa": { + "returns": "any" + } + } + }, + "CallStateEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Mfa" + "returns": "void" }, - "number_groups": { + "call_state": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.NumberGroups" + "returns": "any" }, - "phone_numbers": { + "device": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.PhoneNumbers" + "returns": "any" }, - "project": { + "direction": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.ProjectNamespace" + "returns": "any" }, - "project_id": { + "end_reason": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "pubsub": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "class:signalwire.rest.namespaces.pubsub_resources_generated.PubSub" - }, - "queues": { + "returns": "class:signalwire.relay.event.CallStateEvent" + } + } + }, + "CallingErrorEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Queues" + "returns": "void" }, - "recordings": { + "code": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Recordings" + "returns": "any" }, - "registry": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.RegistryNamespace" + "returns": "class:signalwire.relay.event.CallingErrorEvent" }, - "short_codes": { + "message": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.ShortCodes" - }, - "sip_profile": { + "returns": "any" + } + } + }, + "CollectEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.SipProfile" + "returns": "void" }, - "verified_callers": { + "control_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.VerifiedCallers" + "returns": "any" }, - "video": { + "final": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.rest.namespaces._client_tree_generated.VideoNamespace" + "returns": "any" }, - "with_base_url": { - "params": [ - { - "name": "base_url", - "type": "string", - "required": true - }, - { - "name": "project_id", - "type": "string", - "required": true - }, - { - "name": "token", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.rest.client.RestClient" - } - } - } - } - }, - "signalwire.rest.generated.resource_tree": { - "classes": { - "ResourceTree": { - "methods": { - "__init__": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", + "name": "payload", + "type": "any", "required": true } ], - "returns": "void" - } - } - } - } - }, - "signalwire.rest.namespaces._client_tree_generated": { - "classes": { - "DatasphereNamespace": { - "methods": { - "__init__": { + "returns": "class:signalwire.relay.event.CollectEvent" + }, + "result": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "any" }, - "documents": { + "state": { "params": [ { "name": "self", @@ -18666,32 +18724,18 @@ } } }, - "FabricNamespace": { + "ConferenceEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "addresses": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "ai_agents": { + "conference_id": { "params": [ { "name": "self", @@ -18700,16 +18744,17 @@ ], "returns": "any" }, - "call_flows": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.event.ConferenceEvent" }, - "conference_rooms": { + "name": { "params": [ { "name": "self", @@ -18718,7 +18763,7 @@ ], "returns": "any" }, - "cxml_applications": { + "status": { "params": [ { "name": "self", @@ -18726,17 +18771,21 @@ } ], "returns": "any" - }, - "cxml_scripts": { + } + } + }, + "ConnectEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "cxml_webhooks": { + "connect_state": { "params": [ { "name": "self", @@ -18745,16 +18794,17 @@ ], "returns": "any" }, - "freeswitch_connectors": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.event.ConnectEvent" }, - "relay_applications": { + "peer": { "params": [ { "name": "self", @@ -18762,17 +18812,21 @@ } ], "returns": "any" - }, - "resources": { + } + } + }, + "DenoiseEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "sip_endpoints": { + "denoised": { "params": [ { "name": "self", @@ -18781,25 +18835,30 @@ ], "returns": "any" }, - "sip_gateways": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "any" - }, - "subscribers": { + "returns": "class:signalwire.relay.event.DenoiseEvent" + } + } + }, + "DetectEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "swml_scripts": { + "control_id": { "params": [ { "name": "self", @@ -18808,7 +18867,7 @@ ], "returns": "any" }, - "swml_webhooks": { + "detect": { "params": [ { "name": "self", @@ -18817,34 +18876,30 @@ ], "returns": "any" }, - "tokens": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.event.DetectEvent" } } }, - "LogsNamespace": { + "DialEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "conferences": { + "call": { "params": [ { "name": "self", @@ -18853,7 +18908,7 @@ ], "returns": "any" }, - "fax": { + "dial_state": { "params": [ { "name": "self", @@ -18862,16 +18917,17 @@ ], "returns": "any" }, - "messages": { + "from_payload": { "params": [ { - "name": "self", - "kind": "self" + "name": "payload", + "type": "any", + "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.event.DialEvent" }, - "voice": { + "tag": { "params": [ { "name": "self", @@ -18882,23 +18938,28 @@ } } }, - "ProjectNamespace": { + "EchoEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "from_payload": { + "params": [ { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", + "name": "payload", + "type": "any", "required": true } ], - "returns": "void" + "returns": "class:signalwire.relay.event.EchoEvent" }, - "tokens": { + "state": { "params": [ { "name": "self", @@ -18909,23 +18970,18 @@ } } }, - "RegistryNamespace": { + "FaxEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "brands": { + "control_id": { "params": [ { "name": "self", @@ -18934,7 +18990,7 @@ ], "returns": "any" }, - "campaigns": { + "fax": { "params": [ { "name": "self", @@ -18943,16 +18999,40 @@ ], "returns": "any" }, - "numbers": { + "from_payload": { + "params": [ + { + "name": "payload", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.relay.event.FaxEvent" + } + } + }, + "HoldEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "orders": { + "from_payload": { + "params": [ + { + "name": "payload", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.relay.event.HoldEvent" + }, + "state": { "params": [ { "name": "self", @@ -18963,23 +19043,18 @@ } } }, - "VideoNamespace": { + "MessageReceiveEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "conference_tokens": { + "body": { "params": [ { "name": "self", @@ -18988,7 +19063,7 @@ ], "returns": "any" }, - "conferences": { + "context": { "params": [ { "name": "self", @@ -18997,7 +19072,7 @@ ], "returns": "any" }, - "room_recordings": { + "direction": { "params": [ { "name": "self", @@ -19006,7 +19081,7 @@ ], "returns": "any" }, - "room_sessions": { + "from_number": { "params": [ { "name": "self", @@ -19015,7 +19090,17 @@ ], "returns": "any" }, - "room_tokens": { + "from_payload": { + "params": [ + { + "name": "payload", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.relay.event.MessageReceiveEvent" + }, + "media": { "params": [ { "name": "self", @@ -19024,7 +19109,7 @@ ], "returns": "any" }, - "rooms": { + "message_id": { "params": [ { "name": "self", @@ -19033,7 +19118,34 @@ ], "returns": "any" }, - "streams": { + "message_state": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "segments": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "tags": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "to_number": { "params": [ { "name": "self", @@ -19043,1356 +19155,713 @@ "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.calling_resources_generated": { - "classes": { - "Calling": { + }, + "MessageStateEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "http", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "ai_hold": { + "body": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "timeout", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "prompt", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "ai_message": { + "context": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "role", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_text", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "reset", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "global_data", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "ai_stop": { + "direction": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "ai_unhold": { + "from_number": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "call_id", - "kind": "positional", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "prompt", - "kind": "keyword", - "type": "optional", - "required": false - }, + } + ], + "returns": "class:signalwire.relay.event.MessageStateEvent" + }, + "media": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "collect": { + "message_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "message_state": { + "params": [ { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "initial_timeout", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "digits", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "speech", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "continuous", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "partial_results", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "collect_start_input_timers": { + "reason": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "collect_stop": { + "segments": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "denoise": { + "tags": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "denoise_stop": { + "to_number": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" - }, - "detect": { + } + } + }, + "PayEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "control_id": { + "params": [ { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "detect", - "kind": "keyword", - "type": "dict", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "timeout", - "kind": "keyword", - "type": "optional", - "required": false - }, + } + ], + "returns": "class:signalwire.relay.event.PayEvent" + }, + "state": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" - }, - "detect_stop": { + } + } + }, + "PlayEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "control_id": { + "params": [ { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "control_id", - "kind": "keyword", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, + } + ], + "returns": "class:signalwire.relay.event.PlayEvent" + }, + "state": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" + } + } + }, + "QueueEvent": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "dial": { + "control_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "from", - "kind": "keyword", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "to", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "caller_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "fallback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_events", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "url_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "codecs", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "swml", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], - "returns": "any" + "returns": "class:signalwire.relay.event.QueueEvent" }, - "disconnect": { + "position": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "end": { + "queue_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "reason", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "live_transcribe": { + "queue_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "action", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "live_translate": { + "size": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "action", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "play": { + "status": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "play", - "kind": "keyword", - "type": "list", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "volume", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "direction", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "loop", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" - }, - "play_pause": { + } + } + }, + "RecordEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, + } + ], + "returns": "void" + }, + "control_id": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "play_resume": { + "duration": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "control_id", - "kind": "keyword", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], - "returns": "any" + "returns": "class:signalwire.relay.event.RecordEvent" }, - "play_stop": { + "record": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "play_volume": { + "size": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "volume", - "kind": "keyword", - "type": "float", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "receive_fax_stop": { + "state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "record": { + "url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "audio", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" - }, - "record_pause": { + } + } + }, + "ReferEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "void" + }, + "from_payload": { + "params": [ { - "name": "control_id", - "kind": "keyword", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, + } + ], + "returns": "class:signalwire.relay.event.ReferEvent" + }, + "sip_notify_response_code": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "record_resume": { + "sip_refer_response_code": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "record_stop": { + "sip_refer_to": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "refer": { + "state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "device", - "kind": "keyword", - "type": "dict", - "required": true - }, + } + ], + "returns": "any" + } + } + }, + "RelayEvent": { + "methods": { + "__init__": { + "params": [ { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "call_id": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "send_fax_stop": { + "event_type": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "control_id", - "kind": "keyword", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, + } + ], + "returns": "class:signalwire.relay.event.RelayEvent" + }, + "params": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "stream": { + "timestamp": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "url", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "codec", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "track", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "authorization_bearer_token", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "custom_parameters", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" - }, - "stream_stop": { + } + } + }, + "SendDigitsEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], - "returns": "any" + "returns": "void" }, - "tap": { + "control_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "tap", - "kind": "keyword", - "type": "dict", - "required": true - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "device", - "kind": "keyword", - "type": "dict", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], - "returns": "any" + "returns": "class:signalwire.relay.event.SendDigitsEvent" }, - "tap_stop": { + "state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" + } + } + }, + "StreamEvent": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "transcribe": { + "control_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "call_id", - "kind": "positional", - "type": "string", + "name": "payload", + "type": "any", "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, + } + ], + "returns": "class:signalwire.relay.event.StreamEvent" + }, + "name": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "transcribe_stop": { + "state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "control_id", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" }, - "transfer": { + "url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + } + } + }, + "TapEvent": { + "methods": { + "__init__": { + "params": [ { - "name": "dest", - "kind": "keyword", - "type": "dict", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "control_id": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "update": { + "device": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "fallback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "url", - "kind": "keyword", - "type": "optional", - "required": false - }, + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "swml", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "payload", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.relay.event.TapEvent" + }, + "state": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" }, - "user_event": { + "tap": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "event", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null } ], "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.chat_resources_generated": { - "classes": { - "Chat": { + }, + "TranscribeEvent": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "create_token": { + "control_id": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "duration": { + "params": [ { - "name": "ttl", - "kind": "keyword", - "type": "int", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from_payload": { + "params": [ { - "name": "channels", - "kind": "keyword", - "type": "dict", + "name": "payload", + "type": "any", "required": true - }, + } + ], + "returns": "class:signalwire.relay.event.TranscribeEvent" + }, + "recording_id": { + "params": [ { - "name": "member_id", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "size": { + "params": [ { - "name": "state", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "state": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "url": { + "params": [ { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" @@ -20401,733 +19870,451 @@ } } }, - "signalwire.rest.namespaces.datasphere_resources_generated": { + "signalwire.relay.message": { "classes": { - "DatasphereDocuments": { + "Message": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "create": { + "__repr__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" + "returns": "string" }, - "delete": { + "body": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" }, - "delete_chunk": { + "context": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "documentId", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "chunkId", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" }, - "get_chunk": { + "direction": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "documentId", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "chunkId", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "list_chunks": { + "from_number": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "documentId", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "from_params": { + "params": [ { "name": "params", - "kind": "var_keyword", "type": "any", - "required": false, - "default": {} + "required": true } ], - "returns": "any" + "returns": "class:signalwire.relay.message.Message" }, - "search": { + "is_delivered": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "bool" + }, + "is_done": { + "params": [ { - "name": "query_string", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "tags", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "document_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "distance", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "count", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "language", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "pos_to_expand", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "max_synonyms", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" }, - "update": { + "is_failed": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.fabric_resources_generated": { - "classes": { - "AiAgents": { - "methods": { - "__init__": { + "returns": "bool" + }, + "is_terminal": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "bool" }, - "create": { + "media": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "update": { + "message_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" - } - } - }, - "CallFlows": { - "methods": { - "__init__": { + }, + "message_state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "optional" }, - "create": { + "on": { "params": [ { "name": "self", "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "cb", + "type": "callable,void>", "required": true } ], - "returns": "any" + "returns": "void" }, - "deploy_version": { + "on_completed": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "cb", + "type": "callable,void>", "required": true } ], - "returns": "any" + "returns": "void" }, - "list_addresses": { + "reason": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "string" }, - "list_versions": { + "result": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "update": { + "segments": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" - } - } - }, - "ConferenceRooms": { - "methods": { - "__init__": { + }, + "state": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "string" }, - "create": { + "tags": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "list_addresses": { + "to_number": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "update": { + "update_state": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "new_state", "type": "string", "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" - } - } - }, - "CxmlApplications": { - "methods": { - "__init__": { + "returns": "void" + }, + "wait": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "timeout_ms", + "type": "int", + "required": false, + "default": null } ], - "returns": "void" - }, - "delete": { + "returns": "bool" + } + } + } + } + }, + "signalwire.relay.message_event": { + "classes": { + "MessageEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "from_relay_event": { + "params": [ { - "name": "id", - "kind": "positional", - "type": "string", + "name": "ev", + "type": "class:signalwire.relay.relay_event.RelayEvent", "required": true } ], - "returns": "any" - }, - "get": { + "returns": "class:signalwire.relay.message_event.MessageEvent" + } + } + } + } + }, + "signalwire.relay.relay_event": { + "classes": { + "RelayEvent": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "void" + }, + "from_json": { + "params": [ { - "name": "id", - "kind": "positional", - "type": "string", + "name": "j", + "type": "any", "required": true - }, + } + ], + "returns": "class:signalwire.relay.relay_event.RelayEvent" + } + } + } + } + }, + "signalwire.relay.web_socket_client": { + "classes": { + "WebSocketClient": { + "methods": { + "__init__": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "list": { + "close": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "code", + "type": "int", "required": false, - "default": {} + "default": 1000 + }, + { + "name": "reason", + "type": "string", + "required": false, + "default": "" } ], - "returns": "any" + "returns": "void" }, - "list_addresses": { + "connect": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "host", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "port", + "type": "int", "required": false, - "default": {} + "default": 443 } ], - "returns": "any" + "returns": "bool" }, - "update": { + "connect_plain": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "host", "type": "string", "required": true }, { - "name": "display_name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "account_sid", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "voice_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "voice_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "voice_fallback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "voice_fallback_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_callback", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_callback_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_fallback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_fallback_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_status_callback", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "sms_status_callback_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, + "name": "port", + "type": "int", + "required": true + } + ], + "returns": "bool" + }, + "is_connected": { + "params": [ { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" - } - } - }, - "CxmlScripts": { - "methods": { - "__init__": { + "returns": "bool" + }, + "on_close": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "cb", + "type": "class:signalwire.close_callback.CloseCallback", "required": true } ], "returns": "void" }, - "create": { + "on_error": { "params": [ { "name": "self", "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "cb", + "type": "class:signalwire.error_callback.ErrorCallback", "required": true } ], - "returns": "any" + "returns": "void" }, - "update": { + "on_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "cb", + "type": "class:signalwire.message_callback.MessageCallback", "required": true + } + ], + "returns": "void" + }, + "send": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "message", + "type": "string", "required": true } ], - "returns": "any" + "returns": "bool" } } - }, - "CxmlWebhooks": { + } + } + }, + "signalwire.rest._base": { + "classes": { + "BaseResource": { "methods": { "__init__": { "params": [ @@ -21139,63 +20326,94 @@ "name": "client", "type": "class:signalwire.rest._base.HttpClient", "required": true + }, + { + "name": "base_path", + "type": "string", + "required": true } ], "returns": "void" }, - "create": { + "base_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + } + } + }, + "CrudResource": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + }, + { + "name": "base_path", + "type": "string", "required": true + }, + { + "name": "update_method", + "type": "string", + "required": false, + "default": "PATCH" } ], - "returns": "any" + "returns": "void" }, - "update": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "data", + "type": "any", "required": true }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null, + "kind": "keyword" } ], "returns": "any" - } - } - }, - "FabricAddresses": { - "methods": { - "__init__": { + }, + "delete": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "id", + "type": "string", "required": true + }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null, + "kind": "keyword" } ], - "returns": "void" + "returns": "any" }, "get": { "params": [ @@ -21205,14 +20423,19 @@ }, { "name": "id", - "kind": "positional", "type": "string", "required": true }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null, + "kind": "keyword" + }, { "name": "params", - "kind": "var_keyword", - "type": "any", + "type": "dict", "required": false, "default": {} } @@ -21225,35 +20448,51 @@ "name": "self", "kind": "self" }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null, + "kind": "keyword" + }, { "name": "params", - "kind": "var_keyword", - "type": "any", + "type": "dict", "required": false, "default": {} } ], "returns": "any" }, - "paginate": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "kind": "var_keyword", + "name": "id", + "type": "string", + "required": true + }, + { + "name": "data", "type": "any", + "required": true + }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": {} + "default": null, + "kind": "keyword" } ], "returns": "any" } } }, - "FabricTokens": { + "CrudWithAddresses": { "methods": { "__init__": { "params": [ @@ -21265,290 +20504,265 @@ "name": "client", "type": "class:signalwire.rest._base.HttpClient", "required": true + }, + { + "name": "base_path", + "type": "string", + "required": true + }, + { + "name": "update_method", + "type": "string", + "required": false, + "default": "PATCH" } ], "returns": "void" }, - "create_embed_token": { + "list_addresses": { "params": [ { "name": "self", "kind": "self" }, { - "name": "token", - "kind": "keyword", + "name": "id", "type": "string", "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": null + "default": null, + "kind": "keyword" }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "params", + "type": "dict", "required": false, "default": {} } ], "returns": "any" - }, - "create_guest_token": { + } + } + }, + "HttpClient": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "allowed_addresses", - "kind": "keyword", - "type": "list", + "name": "base_url", + "type": "string", "required": true }, { - "name": "expire_at", - "kind": "keyword", - "type": "optional", - "required": false + "name": "username", + "type": "string", + "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "password", + "type": "string", + "required": true + }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, "default": null + } + ], + "returns": "void" + }, + "base_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "path", + "type": "string", + "required": true + }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "create_invite_token": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "address_id", - "kind": "keyword", - "type": "dict", + "name": "path", + "type": "string", "required": true }, { - "name": "expires_at", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "params", + "type": "dict", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "create_subscriber_token": { + "patch": { "params": [ { "name": "self", "kind": "self" }, { - "name": "reference", - "kind": "keyword", + "name": "path", "type": "string", "required": true }, { - "name": "expire_at", - "kind": "keyword", - "type": "optional", - "required": false + "name": "body", + "type": "any", + "required": false, + "default": null }, { - "name": "application_id", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "post": { + "params": [ { - "name": "password", - "kind": "keyword", - "type": "optional", - "required": false + "name": "self", + "kind": "self" }, { - "name": "first_name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "last_name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "display_name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "job_title", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "time_zone", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "country", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "region", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "company_name", - "kind": "keyword", - "type": "optional", - "required": false + "name": "path", + "type": "string", + "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "body", + "type": "any", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "refresh_subscriber_token": { + "put": { "params": [ { "name": "self", "kind": "self" }, { - "name": "refresh_token", - "kind": "keyword", - "type": "dict", + "name": "path", + "type": "string", "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "body", + "type": "any", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": {} + "default": null } ], "returns": "any" - } - } - }, - "FreeswitchConnectors": { - "methods": { - "__init__": { + }, + "set_ca_cert_path": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "path", + "type": "string", "required": true } ], "returns": "void" }, - "create": { + "set_header": { "params": [ { "name": "self", "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "key", + "type": "string", + "required": true + }, + { + "name": "value", + "type": "string", "required": true } ], - "returns": "any" + "returns": "void" }, - "update": { + "set_timeout": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "seconds", + "type": "int", "required": true } ], - "returns": "any" + "returns": "void" } } }, - "GenericResources": { + "ReadResource": { "methods": { "__init__": { "params": [ @@ -21560,11 +20774,16 @@ "name": "client", "type": "class:signalwire.rest._base.HttpClient", "required": true + }, + { + "name": "base_path", + "type": "string", + "required": true } ], "returns": "void" }, - "assign_domain_application": { + "get": { "params": [ { "name": "self", @@ -21572,206 +20791,207 @@ }, { "name": "id", - "kind": "positional", "type": "string", "required": true }, { - "name": "domain_application_id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": null + "default": null, + "kind": "keyword" }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "params", + "type": "dict", "required": false, "default": {} } ], "returns": "any" }, - "assign_phone_route": { + "list": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "phone_route_id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "handler", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, - "default": null + "default": null, + "kind": "keyword" }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "params", + "type": "dict", "required": false, "default": {} } ], "returns": "any" }, - "delete": { + "paginate": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null, + "kind": "keyword" + }, + { + "name": "params", + "type": "dict", + "required": false, + "default": {} } ], - "returns": "any" - }, - "get": { + "returns": "class:signalwire.rest._pagination.PaginatedIterator" + } + } + }, + "SignalWireRestError": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "status", + "type": "int", + "required": true + }, + { + "name": "message", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "body", + "type": "string", "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ - { - "name": "self", - "kind": "self" + "default": "" }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "url", + "type": "string", "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list_addresses": { - "params": [ - { - "name": "self", - "kind": "self" + "default": "" }, { - "name": "id", - "kind": "positional", + "name": "method", "type": "string", - "required": true + "required": false, + "default": "GET" }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "headers", + "type": "dict", "required": false, "default": {} } ], - "returns": "any" - } - } - }, - "RelayApplications": { - "methods": { - "__init__": { + "returns": "void" + }, + "body": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "string" }, - "create": { + "headers": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "dict" + }, + "method": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "string" }, - "update": { + "request_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "status_code": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + } + } + }, + "SignalWireRestTransportError": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "message", "type": "string", "required": true }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "url", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "method", + "type": "string", + "required": false, + "default": "GET" } ], - "returns": "any" + "returns": "void" } } - }, - "SipEndpoints": { + } + } + }, + "signalwire.rest._pagination": { + "classes": { + "PaginatedIterator": { "methods": { "__init__": { "params": [ @@ -21780,800 +21000,475 @@ "kind": "self" }, { - "name": "client", + "name": "http", "type": "class:signalwire.rest._base.HttpClient", "required": true + }, + { + "name": "path", + "type": "string", + "required": true + }, + { + "name": "params", + "type": "dict", + "required": false, + "default": {} + }, + { + "name": "data_key", + "type": "string", + "required": false, + "default": "data" + }, + { + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false, + "default": null } ], "returns": "void" }, - "create": { + "data_key": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "string" + }, + "done": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" }, - "update": { + "has_next": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "bool" + }, + "http": { + "params": [ { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.rest._base.HttpClient" + }, + "items": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "next": { + "params": [ + { + "name": "self", + "kind": "self" } ], "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "dict" + }, + "path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" } } - }, - "SipGateways": { + } + } + }, + "signalwire.rest._request_options": { + "classes": { + "RequestOptions": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "create": { + "abort_signal": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "update": { + "merge": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "override_opts", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": true - }, + } + ], + "returns": "class:signalwire.rest._request_options.RequestOptions" + }, + "retries": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" } ], "returns": "any" - } - } - }, - "Subscribers": { - "methods": { - "__init__": { + }, + "retry_backoff": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "any" }, - "create": { + "retry_on_status": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "create_sip_endpoint": { + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.client": { + "classes": { + "RestClient": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fabric_subscriber_id", - "kind": "positional", + "name": "space", "type": "string", "required": true }, { - "name": "username", - "kind": "keyword", + "name": "project_id", "type": "string", "required": true }, { - "name": "password", - "kind": "keyword", + "name": "token", "type": "string", "required": true }, { - "name": "caller_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "send_as", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "ciphers", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "codecs", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "encryption", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "void" }, - "delete_sip_endpoint": { + "addresses": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Addresses" }, - "get_sip_endpoint": { + "calling": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.calling_resources_generated.Calling" }, - "list_sip_endpoints": { + "chat": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.chat_resources_generated.Chat" }, - "update": { + "datasphere": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces._client_tree_generated.DatasphereNamespace" }, - "update_sip_endpoint": { + "fabric": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "username", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "password", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "caller_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "send_as", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "ciphers", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "codecs", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "encryption", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" - } - } - }, - "SwmlScripts": { - "methods": { - "__init__": { + "returns": "class:signalwire.rest.namespaces._client_tree_generated.FabricNamespace" + }, + "from_env": { + "params": [], + "returns": "class:signalwire.rest.client.RestClient" + }, + "http_client": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest._base.HttpClient" }, - "create": { + "imported_numbers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.ImportedNumbers" }, - "update": { + "logs": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" - } - } - }, - "SwmlWebhooks": { - "methods": { - "__init__": { + "returns": "class:signalwire.rest.namespaces._client_tree_generated.LogsNamespace" + }, + "lookup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Lookup" }, - "create": { + "messages": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.messages_resources_generated.Messages" }, - "update": { + "mfa": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.fax_resources_generated": { - "classes": { - "FaxLogs": { - "methods": { - "__init__": { + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Mfa" + }, + "number_groups": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.NumberGroups" }, - "get": { + "phone_numbers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.PhoneNumbers" }, - "list": { + "project": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces._client_tree_generated.ProjectNamespace" }, - "paginate": { + "project_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.logs_resources_generated": { - "classes": { - "ConferenceLogs": { - "methods": { - "__init__": { + "returns": "string" + }, + "projects": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest.namespaces.projects_resources_generated.Projects" }, - "list": { + "pubsub": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.message_resources_generated": { - "classes": { - "MessageLogs": { - "methods": { - "__init__": { + "returns": "class:signalwire.rest.namespaces.pubsub_resources_generated.PubSub" + }, + "queues": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Queues" }, - "get": { + "recordings": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.Recordings" }, - "list": { + "registry": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces._client_tree_generated.RegistryNamespace" }, - "paginate": { + "short_codes": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.project_resources_generated": { - "classes": { - "ProjectTokens": { - "methods": { - "__init__": { + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.ShortCodes" + }, + "sip_profile": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.SipProfile" }, - "create": { + "verified_callers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "permissions", - "kind": "keyword", - "type": "list", - "required": true - }, - { - "name": "subproject_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces.relay_rest_resources_generated.VerifiedCallers" }, - "delete": { + "video": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "token_id", - "kind": "positional", - "type": "string", - "required": true } ], - "returns": "any" + "returns": "class:signalwire.rest.namespaces._client_tree_generated.VideoNamespace" }, - "update": { + "with_base_url": { "params": [ { - "name": "self", - "kind": "self" - }, - { - "name": "token_id", - "kind": "positional", + "name": "base_url", "type": "string", "required": true }, { - "name": "name", - "kind": "keyword", - "type": "optional", - "required": false + "name": "project_id", + "type": "string", + "required": true }, { - "name": "permissions", - "kind": "keyword", - "type": "optional", - "required": false + "name": "token", + "type": "string", + "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", + "name": "request_options", + "type": "class:signalwire.rest._request_options.RequestOptions", "required": false, "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "class:signalwire.rest.client.RestClient" } } } } }, - "signalwire.rest.namespaces.pubsub_resources_generated": { + "signalwire.rest.generated.resource_tree": { "classes": { - "PubSub": { + "ResourceTree": { "methods": { "__init__": { "params": [ @@ -22582,67 +21477,47 @@ "kind": "self" }, { - "name": "client", + "name": "http", "type": "class:signalwire.rest._base.HttpClient", "required": true } ], "returns": "void" - }, - "create_token": { + } + } + } + } + }, + "signalwire.rest.namespaces._client_tree_generated": { + "classes": { + "DatasphereNamespace": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "ttl", - "kind": "keyword", - "type": "int", - "required": true - }, - { - "name": "channels", - "kind": "keyword", - "type": "dict", + "name": "http", + "type": "class:signalwire.rest._base.HttpClient", "required": true - }, - { - "name": "member_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "state", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, + } + ], + "returns": "void" + }, + "documents": { + "params": [ { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.relay_rest_resources_generated": { - "classes": { - "Addresses": { + }, + "FabricNamespace": { "methods": { "__init__": { "params": [ @@ -22651,440 +21526,160 @@ "kind": "self" }, { - "name": "client", + "name": "http", "type": "class:signalwire.rest._base.HttpClient", "required": true } ], "returns": "void" }, - "create": { + "addresses": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "label", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "country", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "first_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "last_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "street_number", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "street_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "city", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "state", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "postal_code", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "address_type", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "address_number", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "delete": { + "ai_agents": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" }, - "get": { + "call_flows": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "list": { + "conference_rooms": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" - } - } - }, - "ImportedNumbers": { - "methods": { - "__init__": { + }, + "cxml_applications": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "any" }, - "create": { + "cxml_scripts": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "number", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "number_type", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "capabilities", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" - } - } - }, - "Lookup": { - "methods": { - "__init__": { + }, + "cxml_webhooks": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "any" }, - "phone_number": { + "freeswitch_connectors": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "relay_applications": { + "params": [ { - "name": "e164", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" - } - } - }, - "Mfa": { - "methods": { - "__init__": { + }, + "resources": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "any" }, - "call": { + "sip_endpoints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "to", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "from", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "token_length", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "valid_for", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "max_attempts", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "allow_alphas", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "sms": { + "sip_gateways": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "to", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "from", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "token_length", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "valid_for", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "max_attempts", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "allow_alphas", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "verify": { + "subscribers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "mfa_request_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "swml_scripts": { + "params": [ { - "name": "token", - "kind": "keyword", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swml_webhooks": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "tokens": { + "params": [ { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" } } }, - "NumberGroups": { + "LogsNamespace": { "methods": { "__init__": { "params": [ @@ -23093,161 +21688,133 @@ "kind": "self" }, { - "name": "client", + "name": "http", "type": "class:signalwire.rest._base.HttpClient", "required": true } ], "returns": "void" }, - "add_membership": { + "conferences": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "NumberGroupId", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "phone_number_id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "create": { + "fax": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "delete": { + "messages": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" }, - "delete_membership": { + "voice": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" - }, - "get_membership": { + } + } + }, + "ProjectNamespace": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "http", + "type": "class:signalwire.rest._base.HttpClient", "required": true - }, + } + ], + "returns": "void" + }, + "tokens": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" - }, - "list_memberships": { + } + } + }, + "RegistryNamespace": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "NumberGroupId", - "kind": "positional", - "type": "string", + "name": "http", + "type": "class:signalwire.rest._base.HttpClient", "required": true - }, + } + ], + "returns": "void" + }, + "brands": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" }, - "update": { + "campaigns": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "numbers": { + "params": [ { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "orders": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" } ], "returns": "any" } } }, - "PhoneNumbers": { + "VideoNamespace": { "methods": { "__init__": { "params": [ @@ -23256,664 +21823,739 @@ "kind": "self" }, { - "name": "client", + "name": "http", "type": "class:signalwire.rest._base.HttpClient", "required": true } ], "returns": "void" }, - "create": { + "conference_tokens": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "delete": { + "conferences": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true } ], "returns": "any" }, - "search": { + "room_recordings": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "set_ai_agent": { + "room_sessions": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "any" + }, + "room_tokens": { + "params": [ { - "name": "agent_id", - "kind": "positional", - "type": "dict", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "rooms": { + "params": [ { - "name": "extra", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" }, - "set_call_flow": { + "streams": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.calling_resources_generated": { + "classes": { + "Calling": { + "methods": { + "__init__": { + "params": [ { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true + "name": "self", + "kind": "self" }, { - "name": "flow_id", - "kind": "positional", - "type": "dict", + "name": "http", + "type": "class:signalwire.rest._base.HttpClient", "required": true - }, - { - "name": "version", - "kind": "positional", - "type": "optional", - "required": false - }, - { - "name": "extra", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "void" }, - "set_cxml_application": { + "ai_hold": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "application_id", - "kind": "positional", - "type": "string", - "required": true + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "extra", - "kind": "var_keyword", - "type": "any", + "name": "prompt", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "set_cxml_webhook": { + "ai_message": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "url", - "kind": "positional", - "type": "string", - "required": true + "name": "role", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "fallback_url", - "kind": "positional", + "name": "message_text", + "kind": "keyword", "type": "optional", "required": false }, { - "name": "status_callback_url", - "kind": "positional", + "name": "reset", + "kind": "keyword", "type": "optional", "required": false }, { - "name": "extra", - "kind": "var_keyword", - "type": "any", + "name": "global_data", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "set_relay_application": { + "ai_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "name", - "kind": "positional", + "name": "control_id", + "kind": "keyword", "type": "string", "required": true }, { - "name": "extra", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "set_relay_topic": { + "ai_unhold": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "topic", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "status_callback_url", - "kind": "positional", + "name": "prompt", + "kind": "keyword", "type": "optional", "required": false }, { - "name": "extra", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "set_swml_webhook": { + "collect": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "url", - "kind": "positional", - "type": "string", - "required": true + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "extra", - "kind": "var_keyword", - "type": "any", + "name": "initial_timeout", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "digits", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "speech", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "continuous", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "partial_results", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "update": { + "collect_start_input_timers": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "control_id", + "kind": "keyword", + "type": "string", "required": true - } - ], - "returns": "any" - } - } - }, - "Queues": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "create": { + "collect_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "body", + "name": "call_id", "kind": "positional", - "type": "dict", + "type": "string", + "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "string", "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "delete": { + "denoise": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "get_member": { + "denoise_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "get_next_member": { + "detect": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "detect", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "list_members": { + "detect_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "update": { + "dial": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "from", + "kind": "keyword", + "type": "string", "required": true }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "to", + "kind": "keyword", + "type": "string", "required": true - } - ], - "returns": "any" - } - } - }, - "Recordings": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" - }, - "delete": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "caller_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - } - ], - "returns": "any" - }, - "get": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "fallback_url", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ + "name": "status_events", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "url_method", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - } - } - }, - "RegistryBrands": { - "methods": { - "__init__": { - "params": [ + "name": "url", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "codecs", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ + "name": "swml", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "create_campaign": { + "disconnect": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "get": { + "end": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ + "name": "reason", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "list_campaigns": { + "live_transcribe": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - } - } - }, - "RegistryCampaigns": { - "methods": { - "__init__": { - "params": [ + "name": "action", + "kind": "keyword", + "type": "dict", + "required": true + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "create_order": { + "live_translate": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "phone_numbers", + "name": "action", "kind": "keyword", - "type": "optional", - "required": false + "type": "dict", + "required": true }, { - "name": "status_callback_url", + "name": "status_url", "kind": "keyword", "type": "optional", "required": false @@ -23926,98 +22568,132 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "get": { + "play": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list_numbers": { - "params": [ + "name": "play", + "kind": "keyword", + "type": "list", + "required": true + }, { - "name": "self", - "kind": "self" + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "volume", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "direction", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "loop", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "list_orders": { + "play_pause": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "update": { + "play_resume": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "name", + "name": "control_id", "kind": "keyword", - "type": "optional", - "required": false + "type": "string", + "required": true }, { "name": "extras", @@ -24027,203 +22703,190 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" - } - } - }, - "RegistryNumbers": { - "methods": { - "__init__": { + }, + "play_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "call_id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "delete": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "control_id", + "kind": "keyword", "type": "string", "required": true - } - ], - "returns": "any" - } - } - }, - "RegistryOrders": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "get": { + "play_volume": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - } - } - }, - "ShortCodes": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "volume", + "kind": "keyword", + "type": "float", "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "get": { + "receive_fax_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "update": { + "record": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "message_handler", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "message_request_url", + "name": "control_id", "kind": "keyword", "type": "optional", "required": false }, { - "name": "message_request_method", + "name": "audio", "kind": "keyword", "type": "optional", "required": false }, { - "name": "message_fallback_url", + "name": "status_url", "kind": "keyword", "type": "optional", "required": false }, { - "name": "message_fallback_method", + "name": "extras", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional>", + "required": false, + "default": null }, { - "name": "message_laml_application_id", + "name": "request_options", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "record_pause": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "message_relay_context", + "name": "call_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "control_id", "kind": "keyword", - "type": "optional", - "required": false + "type": "string", + "required": true }, { "name": "extras", @@ -24233,81 +22896,105 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" - } - } - }, - "SipProfile": { - "methods": { - "__init__": { + }, + "record_resume": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "call_id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "get": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "update": { + "record_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "domain_identifier", + "name": "call_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "control_id", "kind": "keyword", - "type": "optional", - "required": false + "type": "string", + "required": true }, { - "name": "default_codecs", + "name": "extras", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional>", + "required": false, + "default": null }, { - "name": "default_ciphers", + "name": "request_options", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "refer": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "default_encryption", + "name": "call_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "device", "kind": "keyword", - "type": "optional", - "required": false + "type": "dict", + "required": true }, { - "name": "default_send_as", + "name": "status_url", "kind": "keyword", "type": "optional", "required": false @@ -24320,92 +23007,129 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" - } - } - }, - "VerifiedCallers": { - "methods": { - "__init__": { + }, + "send_fax_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "call_id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "body", - "kind": "positional", - "type": "dict", + "name": "control_id", + "kind": "keyword", + "type": "string", "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "delete": { + "stream": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true - } - ], - "returns": "any" - }, - "redial_verification": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "url", + "kind": "keyword", "type": "string", "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "codec", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "track", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "authorization_bearer_token", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "custom_parameters", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "submit_verification": { + "stream_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "verification_code", + "name": "control_id", "kind": "keyword", "type": "string", "required": true @@ -24418,142 +23142,152 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "update": { + "tap": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "body", - "kind": "positional", + "name": "tap", + "kind": "keyword", "type": "dict", "required": true - } - ], - "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.video_resources_generated": { - "classes": { - "VideoConferenceTokens": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "device", + "kind": "keyword", + "type": "dict", "required": true + }, + { + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "get": { + "tap_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "reset": { + "transcribe": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true - } - ], - "returns": "any" - } - } - }, - "VideoConferences": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "create_stream": { + "transcribe_stop": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "url", + "name": "control_id", "kind": "keyword", "type": "string", "required": true @@ -24566,53 +23300,51 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "delete": { + "transfer": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true - } - ], - "returns": "any" - }, - "list_conference_tokens": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "dest", + "kind": "keyword", + "type": "dict", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "list_streams": { + "update": { "params": [ { "name": "self", @@ -24620,44 +23352,99 @@ }, { "name": "id", - "kind": "positional", - "type": "string", + "kind": "keyword", + "type": "dict", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "fallback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "swml", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" }, - "update": { + "user_event": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "call_id", "kind": "positional", "type": "string", "required": true }, { - "name": "body", - "kind": "positional", + "name": "event", + "kind": "keyword", "type": "dict", "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], "returns": "any" } } - }, - "VideoRoomRecordings": { + } + } + }, + "signalwire.rest.namespaces.chat_resources_generated": { + "classes": { + "Chat": { "methods": { "__init__": { "params": [ @@ -24673,73 +23460,52 @@ ], "returns": "void" }, - "delete": { + "create_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "ttl", + "kind": "keyword", + "type": "int", "required": true - } - ], - "returns": "any" - }, - "get": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "channels", + "kind": "keyword", + "type": "dict", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ + "name": "member_id", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "state", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list_events": { - "params": [ - { - "name": "self", - "kind": "self" + "default": null }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -24749,8 +23515,12 @@ "returns": "any" } } - }, - "VideoRoomSessions": { + } + } + }, + "signalwire.rest.namespaces.datasphere_resources_generated": { + "classes": { + "DatasphereDocuments": { "methods": { "__init__": { "params": [ @@ -24766,78 +23536,103 @@ ], "returns": "void" }, - "get": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" }, - "list": { + "delete": { "params": [ { "name": "self", "kind": "self" }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "list_events": { + "delete_chunk": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "documentId", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "chunkId", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "list_members": { + "get_chunk": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "documentId", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "chunkId", "kind": "positional", "type": "string", "required": true }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "params", "kind": "var_keyword", @@ -24848,33 +23643,24 @@ ], "returns": "any" }, - "list_recordings": { + "list_chunks": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "documentId", "kind": "positional", "type": "string", "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} - } - ], - "returns": "any" - }, - "paginate": { - "params": [ - { - "name": "self", - "kind": "self" + "default": null }, { "name": "params", @@ -24885,159 +23671,120 @@ } ], "returns": "any" - } - } - }, - "VideoRoomTokens": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, - "create": { + "search": { "params": [ { "name": "self", "kind": "self" }, { - "name": "room_name", + "name": "query_string", "kind": "keyword", "type": "string", "required": true }, { - "name": "user_name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "permissions", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "join_from", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "join_until", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "remove_at", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "remove_after_seconds_elapsed", + "name": "tags", "kind": "keyword", "type": "optional", "required": false }, { - "name": "join_audio_muted", + "name": "document_id", "kind": "keyword", "type": "optional", "required": false }, { - "name": "join_video_muted", + "name": "distance", "kind": "keyword", "type": "optional", "required": false }, { - "name": "auto_create_room", + "name": "count", "kind": "keyword", "type": "optional", "required": false }, { - "name": "enable_room_previews", + "name": "language", "kind": "keyword", "type": "optional", "required": false }, { - "name": "room_display_name", + "name": "pos_to_expand", "kind": "keyword", "type": "optional", "required": false }, { - "name": "end_room_session_on_leave", + "name": "max_synonyms", "kind": "keyword", "type": "optional", "required": false }, { - "name": "join_as", + "name": "extras", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional>", + "required": false, + "default": null }, { - "name": "media_allowed", + "name": "request_options", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional", + "required": false, + "default": null }, { - "name": "room_meta", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ { - "name": "meta", - "kind": "keyword", - "type": "optional", - "required": false + "name": "self", + "kind": "self" }, { - "name": "sync_audio_video", - "kind": "keyword", - "type": "optional", - "required": false + "name": "id", + "kind": "positional", + "type": "string", + "required": true }, { - "name": "extras", + "name": "request_options", "kind": "keyword", - "type": "optional>", + "type": "optional", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" } } - }, - "VideoRooms": { + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated": { + "classes": { + "AiAgents": { "methods": { "__init__": { "params": [ @@ -25060,15 +23807,22 @@ "kind": "self" }, { - "name": "body", - "kind": "positional", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", "type": "dict", "required": true } ], "returns": "any" }, - "create_stream": { + "update": { "params": [ { "name": "self", @@ -25081,29 +23835,90 @@ "required": true }, { - "name": "url", + "name": "request_options", "kind": "keyword", - "type": "string", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true + } + ], + "returns": "any" + } + } + }, + "CallFlows": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "extras", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", "kind": "keyword", - "type": "optional>", + "type": "optional", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "deploy_version": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "delete": { + "list_addresses": { "params": [ { "name": "self", @@ -25114,11 +23929,25 @@ "kind": "positional", "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "list_streams": { + "list_versions": { "params": [ { "name": "self", @@ -25130,6 +23959,13 @@ "type": "string", "required": true }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "params", "kind": "var_keyword", @@ -25152,6 +23988,13 @@ "type": "string", "required": true }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "body", "kind": "positional", @@ -25163,7 +24006,7 @@ } } }, - "VideoStreams": { + "ConferenceRooms": { "methods": { "__init__": { "params": [ @@ -25179,22 +24022,29 @@ ], "returns": "void" }, - "delete": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", "kind": "positional", - "type": "string", + "type": "dict", "required": true } ], "returns": "any" }, - "get": { + "list_addresses": { "params": [ { "name": "self", @@ -25206,6 +24056,13 @@ "type": "string", "required": true }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "params", "kind": "var_keyword", @@ -25229,35 +24086,24 @@ "required": true }, { - "name": "url", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", + "name": "request_options", "kind": "keyword", - "type": "optional>", + "type": "optional", "required": false, "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.voice_resources_generated": { - "classes": { - "VoiceLogs": { + }, + "CxmlApplications": { "methods": { "__init__": { "params": [ @@ -25273,7 +24119,7 @@ ], "returns": "void" }, - "get": { + "delete": { "params": [ { "name": "self", @@ -25286,21 +24132,34 @@ "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, - "default": {} + "default": null } ], "returns": "any" }, - "list": { + "get": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "params", "kind": "var_keyword", @@ -25311,17 +24170,18 @@ ], "returns": "any" }, - "list_events": { + "list": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { "name": "params", @@ -25333,12 +24193,25 @@ ], "returns": "any" }, - "paginate": { + "list_addresses": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "params", "kind": "var_keyword", @@ -25348,851 +24221,1118 @@ } ], "returns": "any" - } - } - } - } - }, - "signalwire.skills.registry": { - "classes": { - "SkillRegistry": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "add_skill_directory": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", + "name": "id", + "kind": "positional", "type": "string", "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "display_name", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "name", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.core.skill_base.SkillBase" - }, - "discover_skills": { - "params": [ + "name": "account_sid", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "external_paths": { - "params": [ + "name": "voice_url", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "list" - }, - "get_all_skills_schema": { - "params": [ + "name": "voice_method", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "voice_fallback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "voice_fallback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_callback", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_callback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_fallback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_fallback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_status_callback", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sms_status_callback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" - }, - "get_skill_class": { + } + } + }, + "CxmlScripts": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true } ], - "returns": "bool" + "returns": "void" }, - "has_skill": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true - } - ], - "returns": "bool" - }, - "instance": { - "params": [], - "returns": "class:signalwire.skills.registry.SkillRegistry" - }, - "list_all_skill_sources": { - "params": [ + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" }, - "list_skills": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "list" - }, - "register_skill": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "id", + "kind": "positional", "type": "string", "required": true }, { - "name": "factory", - "type": "callable,class:signalwire.core.skill_base.SkillBase>", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], - "returns": "void" + "returns": "any" } } - } - } - }, - "signalwire.swaig.parameter_schema": { - "classes": { - "ParameterSchema": { + }, + "CxmlWebhooks": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], "returns": "void" }, - "array_of": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "item_type", - "type": "string", + "name": "body", + "kind": "positional", + "type": "dict", "required": true - }, - { - "name": "description", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + "returns": "any" }, - "boolean": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "id", + "kind": "positional", "type": "string", "required": true }, { - "name": "description", - "type": "string", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" - }, - "empty": { + "returns": "any" + } + } + }, + "FabricAddresses": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], - "returns": "bool" + "returns": "void" }, - "enum_of": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "id", + "kind": "positional", "type": "string", "required": true }, { - "name": "values", - "type": "list", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "description", - "type": "string", + "name": "params", + "kind": "var_keyword", + "type": "any", "required": false, - "default": null + "default": {} } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + "returns": "any" }, - "integer": { + "list": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "description", - "type": "string", + "name": "params", + "kind": "var_keyword", + "type": "any", "required": false, - "default": null + "default": {} } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + "returns": "any" }, - "number": { + "paginate": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "description", - "type": "string", + "name": "params", + "kind": "var_keyword", + "type": "any", "required": false, - "default": null + "default": {} } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" - }, - "object_of": { + "returns": "any" + } + } + }, + "FabricTokens": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "nested", - "type": "class:signalwire.swaig.parameter_schema.ParameterSchema", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true - }, - { - "name": "description", - "type": "string", - "required": false, - "default": null } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + "returns": "void" }, - "property": { + "create_embed_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "token", + "kind": "keyword", "type": "string", "required": true }, { - "name": "schema", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" - }, - "require": { - "params": [ + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "name", - "type": "string", - "required": true + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + "returns": "any" }, - "required": { + "create_guest_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "names", - "type": "list", + "name": "allowed_addresses", + "kind": "keyword", + "type": "list", "required": true - } - ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" - }, - "string": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "expire_at", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "name", - "type": "string", - "required": true + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "description", - "type": "string", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, "default": null - } - ], - "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" - }, - "to_json": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" - } - } - } - } - }, - "signalwire.swml.document": { - "classes": { - "Document": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "add_verb": { + "create_invite_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", - "type": "string", + "name": "address_id", + "kind": "keyword", + "type": "dict", "required": true }, { - "name": "params", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.swml.document.Document" - }, - "add_verb_to_section": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "expires_at", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "section_name", - "type": "string", - "required": true + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { - "name": "verb_name", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "params", + "name": "kwargs", + "kind": "var_keyword", "type": "any", - "required": true + "required": false, + "default": {} } ], - "returns": "class:signalwire.swml.document.Document" + "returns": "any" }, - "has_section": { + "create_subscriber_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "reference", + "kind": "keyword", "type": "string", "required": true - } - ], - "returns": "bool" - }, - "main": { - "params": [ + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "class:signalwire.pom.pom.Section" - }, - "section": { - "params": [ + "name": "expire_at", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "application_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "name", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.pom.pom.Section" - }, - "set_version": { - "params": [ + "name": "password", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "first_name", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "version", - "type": "string", - "required": true - } - ], - "returns": "class:signalwire.swml.document.Document" - }, - "to_json": { - "params": [ + "name": "last_name", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" - } - ], + "name": "display_name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "job_title", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "time_zone", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "country", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "region", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "company_name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], "returns": "any" }, - "to_string": { + "refresh_subscriber_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "indent", - "type": "int", + "name": "refresh_token", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "string" + "returns": "any" } } - } - } - }, - "signalwire.swml.schema": { - "classes": { - "Schema": { + }, + "FreeswitchConnectors": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], "returns": "void" }, - "find_verb": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", - "type": "string", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], - "returns": "class:signalwire.swml.verb_definition.VerbDefinition" + "returns": "any" }, - "load_embedded": { + "update": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], - "returns": "bool" - }, - "load_from_file": { + "returns": "any" + } + } + }, + "GenericResources": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "path", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true } ], - "returns": "bool" + "returns": "void" }, - "load_from_string": { + "assign_domain_application": { "params": [ { "name": "self", "kind": "self" }, { - "name": "schema_json", + "name": "id", + "kind": "positional", "type": "string", "required": true - } - ], - "returns": "bool" - }, - "raw": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "domain_application_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "verb_definitions": { + "assign_phone_route": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "phone_route_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "handler", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "list" + "returns": "any" }, - "verb_names": { + "delete": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "list" - } - } - } - } - }, - "signalwire.swml.section": { - "classes": { - "Section": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "void" + "returns": "any" }, - "add_verb": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb", - "type": "class:signalwire.swml.verb.Verb", + "name": "id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "to_json": { - "params": [ + }, { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - } - } - } - } - }, - "signalwire.swml.verb": { - "classes": { - "Verb": { - "methods": { - "__init__": { - "params": [ + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { - "name": "self", - "kind": "self" + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "void" + "returns": "any" }, - "to_json": { + "list": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" - } - } - } - } - }, - "signalwire.utils": { - "functions": { - "is_serverless_mode": { - "params": [], - "returns": "bool" - } - } - }, - "signalwire.utils.schema_utils": { - "classes": { - "SchemaUtils": { - "methods": { - "__init__": { + }, + "list_addresses": { "params": [ { "name": "self", "kind": "self" }, { - "name": "schema_path", + "name": "id", + "kind": "positional", "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, "default": null }, { - "name": "schema_validation", - "type": "bool", + "name": "params", + "kind": "var_keyword", + "type": "any", "required": false, - "default": null + "default": {} } ], - "returns": "void" - }, - "full_validation_available": { + "returns": "any" + } + } + }, + "RelayApplications": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], - "returns": "bool" + "returns": "void" }, - "generate_method_body": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", - "type": "string", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], - "returns": "string" + "returns": "any" }, - "generate_method_signature": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", + "name": "id", + "kind": "positional", "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], - "returns": "string" - }, - "get_all_verb_names": { + "returns": "any" + } + } + }, + "SipEndpoints": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], - "returns": "list" + "returns": "void" }, - "get_verb_parameters": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", - "type": "string", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], "returns": "any" }, - "get_verb_properties": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", + "name": "id", + "kind": "positional", "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" - }, - "get_verb_required_properties": { + } + } + }, + "SipGateways": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true } ], - "returns": "list" + "returns": "void" }, - "load_schema": { + "create": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "any" - }, - "validate_document": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "document", - "type": "any", + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], - "returns": "tuple>" + "returns": "any" }, - "validate_verb": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "verb_name", + "name": "id", + "kind": "positional", "type": "string", "required": true }, { - "name": "verb_config", - "type": "any", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", "required": true } ], - "returns": "tuple>" + "returns": "any" } } }, - "SchemaValidationError": { + "Subscribers": { "methods": { "__init__": { "params": [ @@ -26201,177 +25341,8288 @@ "kind": "self" }, { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "errors", - "type": "list", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true } ], "returns": "void" }, - "errors": { + "create": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], - "returns": "list" + "returns": "any" }, - "verb_name": { + "create_sip_endpoint": { "params": [ { "name": "self", "kind": "self" - } - ], - "returns": "string" - } - } - } - } - }, - "signalwire.utils.url_validator": { - "functions": { - "validate_url": { - "params": [ - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "allow_private", - "type": "bool", - "required": false, - "default": null - } - ], - "returns": "bool" - } - } - }, - "signalwire.web.web_service": { - "classes": { - "WebService": { - "methods": { - "__init__": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "username", + "kind": "keyword", + "type": "string", + "required": true }, { - "name": "_", - "type": "class:signalwire.web.web_service.WebService", + "name": "password", + "kind": "keyword", + "type": "string", "required": true + }, + { + "name": "caller_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "void" + "returns": "any" }, - "add_directory": { + "delete_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, { - "name": "route", + "name": "fabric_subscriber_id", + "kind": "positional", "type": "string", "required": true }, { - "name": "directory", + "name": "id", + "kind": "positional", "type": "string", "required": true - } - ], - "returns": "void" - }, - "directories": { - "params": [ + }, { - "name": "self", - "kind": "self" + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null } ], - "returns": "dict" + "returns": "any" }, - "file_allowed": { + "get_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, { - "name": "file_path", + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "id", + "kind": "positional", "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "bool" + "returns": "any" }, - "port": { + "list_sip_endpoints": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "int" + "returns": "any" }, - "remove_directory": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "route", + "name": "id", + "kind": "positional", "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], - "returns": "void" + "returns": "any" }, - "start": { + "update_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, { - "name": "host", + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "id", + "kind": "positional", "type": "string", + "required": true + }, + { + "name": "username", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "password", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "caller_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, "default": null }, { - "name": "bind_port", - "type": "optional", + "name": "request_options", + "kind": "keyword", + "type": "optional", "required": false, "default": null - } - ], - "returns": "int" + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "SwmlScripts": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, - "stop": { + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "SwmlWebhooks": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.fax_resources_generated": { + "classes": { + "FaxLogs": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true } ], "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" } } } } - } - }, - "baseline_version": "3.0.2" + }, + "signalwire.rest.namespaces.logs_resources_generated": { + "classes": { + "ConferenceLogs": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.message_resources_generated": { + "classes": { + "MessageLogs": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.messages_resources_generated": { + "classes": { + "Messages": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "to", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "from", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "body", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "media", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "send_as_mms", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_callback", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "custom_variables", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "message_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "body", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.project_resources_generated": { + "classes": { + "ProjectTokens": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "permissions", + "kind": "keyword", + "type": "list", + "required": true + }, + { + "name": "subproject_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "token_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "token_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "permissions", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.projects_resources_generated": { + "classes": { + "Projects": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "rotate_signing_key": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.pubsub_resources_generated": { + "classes": { + "PubSub": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "ttl", + "kind": "keyword", + "type": "int", + "required": true + }, + { + "name": "channels", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "member_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "state", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated": { + "classes": { + "Addresses": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "label", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "country", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "first_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "last_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "street_number", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "street_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "city", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "state", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "postal_code", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "address_type", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "address_number", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "ImportedNumbers": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "number", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "number_type", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "capabilities", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "Lookup": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "phone_number": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "e164", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "Mfa": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "call": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "to", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "from", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "token_length", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "valid_for", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "max_attempts", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "allow_alphas", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "sms": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "to", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "from", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "token_length", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "valid_for", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "max_attempts", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "allow_alphas", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "verify": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "mfa_request_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "token", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "NumberGroups": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "add_membership": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "NumberGroupId", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "phone_number_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "delete_membership": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get_membership": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_memberships": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "NumberGroupId", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "PhoneNumbers": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "search": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_ai_agent": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "agent_id", + "kind": "positional", + "type": "dict", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_call_flow": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "flow_id", + "kind": "positional", + "type": "dict", + "required": true + }, + { + "name": "version", + "kind": "positional", + "type": "optional", + "required": false + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_cxml_application": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "application_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_cxml_webhook": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "url", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "fallback_url", + "kind": "positional", + "type": "optional", + "required": false + }, + { + "name": "status_callback_url", + "kind": "positional", + "type": "optional", + "required": false + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_relay_application": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "name", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_relay_topic": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "topic", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "status_callback_url", + "kind": "positional", + "type": "optional", + "required": false + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_swml_webhook": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "url", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "Queues": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get_member": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "queue_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "get_next_member": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "queue_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_members": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "queue_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "Recordings": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "RegistryBrands": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "create_campaign": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_campaigns": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "RegistryCampaigns": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create_order": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "phone_numbers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "status_callback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_numbers": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_orders": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "RegistryNumbers": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + } + } + }, + "RegistryOrders": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "ShortCodes": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "message_handler", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "message_request_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_request_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_fallback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_fallback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_laml_application_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_relay_context", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "SipProfile": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "domain_identifier", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "VerifiedCallers": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "redial_verification": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "submit_verification": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "verification_code", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.video_resources_generated": { + "classes": { + "VideoConferenceTokens": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "reset": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + } + } + }, + "VideoConferences": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "create_stream": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "url", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "list_conference_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_streams": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "VideoRoomRecordings": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_events": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "VideoRoomSessions": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_events": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_members": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_recordings": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "VideoRoomTokens": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "room_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "user_name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "permissions", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_from", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_until", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "remove_at", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "remove_after_seconds_elapsed", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_audio_muted", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_video_muted", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "auto_create_room", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "enable_room_previews", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "room_display_name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "end_room_session_on_leave", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "media_allowed", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "room_meta", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "meta", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sync_audio_video", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "VideoRooms": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "create_stream": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "url", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "list_streams": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "VideoStreams": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "url", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.voice_resources_generated": { + "classes": { + "VoiceLogs": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_events": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.skills.registry": { + "classes": { + "SkillRegistry": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "add_skill_directory": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "path", + "type": "string", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.skill_base.SkillBase" + }, + "discover_skills": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "external_paths": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_all_skills_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_skill_class": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "has_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "instance": { + "params": [], + "returns": "class:signalwire.skills.registry.SkillRegistry" + }, + "list_all_skill_sources": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "list_skills": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "register_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "factory", + "type": "callable,class:signalwire.core.skill_base.SkillBase>", + "required": true + } + ], + "returns": "void" + } + } + } + } + }, + "signalwire.skills.spider.skill": { + "classes": { + "SpiderSkill": { + "methods": { + "remove_xpaths": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + } + } + } + } + }, + "signalwire.swaig.parameter_schema": { + "classes": { + "ParameterSchema": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "array_of": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "item_type", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "boolean": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "empty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "enum_of": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "values", + "type": "list", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "integer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "number": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "object_of": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "nested", + "type": "class:signalwire.swaig.parameter_schema.ParameterSchema", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "property": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "schema", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "require": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "required": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "names", + "type": "list", + "required": true + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "string": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" + }, + "to_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.swml.document": { + "classes": { + "Document": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "add_verb": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "params", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.swml.document.Document" + }, + "add_verb_to_section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "section_name", + "type": "string", + "required": true + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "params", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.swml.document.Document" + }, + "has_section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "main": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.pom.pom.Section" + }, + "section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.pom.pom.Section" + }, + "set_version": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "version", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.swml.document.Document" + }, + "to_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "to_string": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "indent", + "type": "int", + "required": false, + "default": -1 + } + ], + "returns": "string" + } + } + } + } + }, + "signalwire.swml.schema": { + "classes": { + "Schema": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "find_verb": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.swml.verb_definition.VerbDefinition" + }, + "load_embedded": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "load_from_file": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "path", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "load_from_string": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "schema_json", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "raw": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "verb_definitions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "verb_names": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + } + } + } + } + }, + "signalwire.swml.section": { + "classes": { + "Section": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "add_verb": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb", + "type": "class:signalwire.swml.verb.Verb", + "required": true + } + ], + "returns": "void" + }, + "to_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.swml.verb": { + "classes": { + "Verb": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "to_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.utils": { + "functions": { + "is_serverless_mode": { + "params": [], + "returns": "bool" + } + } + }, + "signalwire.utils.schema_utils": { + "classes": { + "SchemaUtils": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "schema_path", + "type": "string", + "required": false, + "default": "" + }, + { + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "void" + }, + "full_validation_available": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "generate_method_body": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "string" + }, + "generate_method_signature": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "string" + }, + "get_all_verb_names": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_verb_parameters": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "any" + }, + "get_verb_properties": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "any" + }, + "get_verb_required_properties": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "list" + }, + "load_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "schema_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "validate_document": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "document", + "type": "any", + "required": true + } + ], + "returns": "tuple>" + }, + "validate_verb": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "verb_config", + "type": "any", + "required": true + } + ], + "returns": "tuple>" + }, + "validate_verb_top_level_keys": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "verb_config", + "type": "any", + "required": true + } + ], + "returns": "tuple>" + } + } + }, + "SchemaValidationError": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "errors", + "type": "list", + "required": true + } + ], + "returns": "void" + }, + "errors": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "verb_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + } + } + } + } + }, + "signalwire.utils.url_validator": { + "functions": { + "validate_url": { + "params": [ + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "allow_private", + "type": "bool", + "required": false, + "default": false + } + ], + "returns": "bool" + } + } + }, + "signalwire.web.web_service": { + "classes": { + "WebService": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "port", + "type": "int", + "required": false, + "default": 8002 + }, + { + "name": "directories", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "basic_auth", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "enable_directory_browsing", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "allowed_extensions", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "blocked_extensions", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "max_file_size", + "type": "int", + "required": false, + "default": null + }, + { + "name": "enable_cors", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "void" + }, + "add_directory": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "route", + "type": "string", + "required": true + }, + { + "name": "directory", + "type": "string", + "required": true + } + ], + "returns": "void" + }, + "allowed_extensions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional>" + }, + "blocked_extensions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "directories": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "dict" + }, + "enable_cors": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "enable_directory_browsing": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "file_allowed": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "file_path", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "max_file_size": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "port": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "remove_directory": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "route", + "type": "string", + "required": true + } + ], + "returns": "void" + }, + "start": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "bind_port", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "int" + }, + "stop": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + } + } + } + } + } + }, + "baseline_version": "3.0.0", + "construction": { + "signalwire.agent.language_config.LanguageConfig": { + "params": { + "code": { + "type": "string", + "required": false + }, + "engine": { + "type": "string", + "required": false + }, + "function_fillers": { + "type": "list", + "required": false + }, + "model": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "params": { + "type": "any", + "required": false + }, + "speech_fillers": { + "type": "list", + "required": false + }, + "voice": { + "type": "string", + "required": false + } + } + }, + "signalwire.agent.pronunciation.Pronunciation": { + "params": { + "ignore_case": { + "type": "bool", + "required": false + }, + "replace_val": { + "type": "string", + "required": false + }, + "with_val": { + "type": "string", + "required": false + } + } + }, + "signalwire.agent_server.AgentServer": { + "params": { + "host": { + "type": "string", + "required": false + }, + "log_level": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "static_dir": { + "type": "string", + "required": false + } + } + }, + "signalwire.agents.bedrock.BedrockAgent": { + "params": { + "llm_model": { + "type": "string", + "required": false + }, + "llm_temperature": { + "type": "float", + "required": false + }, + "max_tokens": { + "type": "int", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "post_prompt_llm_params": { + "type": "any", + "required": false + }, + "prompt_llm_params": { + "type": "any", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "system_prompt": { + "type": "string", + "required": false + }, + "temperature": { + "type": "float", + "required": false + }, + "top_p": { + "type": "float", + "required": false + }, + "voice": { + "type": "string", + "required": false + }, + "voice_id": { + "type": "string", + "required": false + } + } + }, + "signalwire.ai_chat.client.AIChatClient": { + "params": { + "project": { + "type": "optional", + "required": false + }, + "space": { + "type": "optional", + "required": false + }, + "token": { + "type": "optional", + "required": false + }, + "url": { + "type": "optional", + "required": false + } + } + }, + "signalwire.ai_chat.client.AIChatError": { + "params": { + "code": { + "type": "optional", + "required": true + }, + "message": { + "type": "string", + "required": true + } + } + }, + "signalwire.ai_chat.client.ChatLog": { + "params": { + "call_timeline": { + "type": "list>", + "required": false + }, + "messages": { + "type": "list>", + "required": false + } + } + }, + "signalwire.ai_chat.client.ChatResponse": { + "params": { + "conversation_id": { + "type": "string", + "required": true + }, + "text": { + "type": "string", + "required": true + }, + "user_event": { + "type": "optional>", + "required": false + } + } + }, + "signalwire.ai_chat.client.ConversationInfo": { + "params": { + "id": { + "type": "string", + "required": true + }, + "initial_message": { + "type": "optional", + "required": false + }, + "status": { + "type": "string", + "required": true + } + } + }, + "signalwire.core.agent.prompt.manager.PromptManager": { + "params": { + "agent_id": { + "type": "optional", + "required": false + }, + "auto_answer": { + "type": "bool", + "required": false + }, + "basic_auth": { + "type": "optional>", + "required": false + }, + "check_for_input_override": { + "type": "bool", + "required": false + }, + "config_file": { + "type": "optional", + "required": false + }, + "default_webhook_url": { + "type": "optional", + "required": false + }, + "enable_post_prompt_override": { + "type": "bool", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "native_functions": { + "type": "optional>", + "required": false + }, + "port": { + "type": "optional", + "required": false + }, + "post_prompt": { + "type": "string", + "required": false + }, + "prompt_pom": { + "type": "list", + "required": false + }, + "prompt_text": { + "type": "string", + "required": false + }, + "record_call": { + "type": "bool", + "required": false + }, + "record_format": { + "type": "string", + "required": false + }, + "record_stereo": { + "type": "bool", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "schema_path": { + "type": "optional", + "required": false + }, + "schema_validation": { + "type": "bool", + "required": false + }, + "signing_key": { + "type": "optional", + "required": false + }, + "suppress_logs": { + "type": "bool", + "required": false + }, + "token_expiry_secs": { + "type": "int", + "required": false + }, + "trust_proxy_for_signature": { + "type": "bool", + "required": false + }, + "use_pom": { + "type": "bool", + "required": false + } + } + }, + "signalwire.core.agent.tools.registry.ToolRegistry": { + "params": { + "agent_id": { + "type": "optional", + "required": false + }, + "auto_answer": { + "type": "bool", + "required": false + }, + "basic_auth": { + "type": "optional>", + "required": false + }, + "check_for_input_override": { + "type": "bool", + "required": false + }, + "config_file": { + "type": "optional", + "required": false + }, + "default_webhook_url": { + "type": "optional", + "required": false + }, + "enable_post_prompt_override": { + "type": "bool", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "native_functions": { + "type": "optional>", + "required": false + }, + "port": { + "type": "optional", + "required": false + }, + "record_call": { + "type": "bool", + "required": false + }, + "record_format": { + "type": "string", + "required": false + }, + "record_stereo": { + "type": "bool", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "schema_path": { + "type": "optional", + "required": false + }, + "schema_validation": { + "type": "bool", + "required": false + }, + "signing_key": { + "type": "optional", + "required": false + }, + "suppress_logs": { + "type": "bool", + "required": false + }, + "token_expiry_secs": { + "type": "int", + "required": false + }, + "trust_proxy_for_signature": { + "type": "bool", + "required": false + }, + "use_pom": { + "type": "bool", + "required": false + } + } + }, + "signalwire.core.agent_base.AgentBase": { + "params": { + "agent_id": { + "type": "optional", + "required": false + }, + "auto_answer": { + "type": "bool", + "required": false + }, + "basic_auth": { + "type": "optional>", + "required": false + }, + "check_for_input_override": { + "type": "bool", + "required": false + }, + "config_file": { + "type": "optional", + "required": false + }, + "default_webhook_url": { + "type": "optional", + "required": false + }, + "enable_post_prompt_override": { + "type": "bool", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "native_functions": { + "type": "optional>", + "required": false + }, + "port": { + "type": "optional", + "required": false + }, + "post_prompt_url": { + "type": "string", + "required": false + }, + "post_prompt_url_direct": { + "type": "string", + "required": false + }, + "record_call": { + "type": "bool", + "required": false + }, + "record_format": { + "type": "string", + "required": false + }, + "record_stereo": { + "type": "bool", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "schema_path": { + "type": "optional", + "required": false + }, + "schema_validation": { + "type": "bool", + "required": false + }, + "signing_key": { + "type": "optional", + "required": false + }, + "suppress_logs": { + "type": "bool", + "required": false + }, + "token_expiry_secs": { + "type": "int", + "required": false + }, + "trust_proxy_for_signature": { + "type": "bool", + "required": false + }, + "use_pom": { + "type": "bool", + "required": false + }, + "web_hook_url": { + "type": "string", + "required": false + }, + "webhook_url": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.auth_exception.AuthException": { + "params": { + "body": { + "type": "string", + "required": false + }, + "headers": { + "type": "any", + "required": false + }, + "status": { + "type": "int", + "required": false + } + } + }, + "signalwire.core.auth_handler.AuthHandler": { + "params": { + "security_config": { + "type": "class:signalwire.core.security_config.SecurityConfig", + "required": true + } + } + }, + "signalwire.core.auth_handler.BasicCredentials": { + "params": { + "password": { + "type": "string", + "required": false + }, + "username": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.auth_handler.BearerCredentials": { + "params": { + "credentials": { + "type": "string", + "required": false + }, + "scheme": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.config_loader.ConfigLoader": { + "params": { + "config_paths": { + "type": "optional>", + "required": false + } + } + }, + "signalwire.core.contexts.Context": { + "params": { + "consolidate": { + "type": "bool", + "required": false + }, + "enter_fillers": { + "type": "any", + "required": false + }, + "exit_fillers": { + "type": "any", + "required": false + }, + "full_reset": { + "type": "bool", + "required": false + }, + "history": { + "type": "string", + "required": false + }, + "initial_step": { + "type": "string", + "required": false + }, + "isolated": { + "type": "bool", + "required": false + }, + "post_prompt": { + "type": "string", + "required": false + }, + "prompt": { + "type": "string", + "required": false + }, + "system_prompt": { + "type": "string", + "required": false + }, + "user_prompt": { + "type": "string", + "required": false + }, + "valid_contexts": { + "type": "list", + "required": false + }, + "valid_steps": { + "type": "list", + "required": false + } + } + }, + "signalwire.core.contexts.GatherInfo": { + "params": { + "completion_action": { + "type": "optional", + "required": false + }, + "isolated": { + "type": "bool", + "required": false + }, + "output_key": { + "type": "optional", + "required": false + }, + "prompt": { + "type": "optional", + "required": false + } + } + }, + "signalwire.core.contexts.GatherQuestion": { + "params": { + "confirm": { + "type": "bool", + "required": false + }, + "functions": { + "type": "list", + "required": false + }, + "isolated": { + "type": "optional", + "required": false + }, + "key": { + "type": "string", + "required": true + }, + "prompt": { + "type": "optional", + "required": false + }, + "question": { + "type": "string", + "required": true + }, + "type": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.contexts.Step": { + "params": { + "end": { + "type": "bool", + "required": false + }, + "functions": { + "type": "union>", + "required": false + }, + "history": { + "type": "string", + "required": false + }, + "reset_consolidate": { + "type": "bool", + "required": false + }, + "reset_full_reset": { + "type": "bool", + "required": false + }, + "reset_system_prompt": { + "type": "string", + "required": false + }, + "reset_user_prompt": { + "type": "string", + "required": false + }, + "skip_to_next_step": { + "type": "bool", + "required": false + }, + "skip_user_turn": { + "type": "bool", + "required": false + }, + "step_criteria": { + "type": "string", + "required": false + }, + "text": { + "type": "string", + "required": false + }, + "valid_contexts": { + "type": "list", + "required": false + }, + "valid_steps": { + "type": "list", + "required": false + } + } + }, + "signalwire.core.data_map.DataMap": { + "params": { + "function_name": { + "type": "string", + "required": true + } + } + }, + "signalwire.core.function_result.FunctionResult": { + "params": { + "end_of_speech_timeout": { + "type": "int", + "required": false + }, + "metadata": { + "type": "any", + "required": false + }, + "post_process": { + "type": "bool", + "required": false + }, + "response": { + "type": "string", + "required": false + }, + "speech_event_timeout": { + "type": "int", + "required": false + } + } + }, + "signalwire.core.mixins.ai_config_mixin.AIConfigMixin": { + "params": { + "function_includes": { + "type": "list", + "required": false + }, + "global_data": { + "type": "any", + "required": false + }, + "internal_fillers": { + "type": "any", + "required": false + }, + "languages": { + "type": "list", + "required": false + }, + "multilingual": { + "type": "any", + "required": false + }, + "native_functions": { + "type": "list", + "required": false + }, + "params": { + "type": "any", + "required": false + }, + "post_prompt_llm_params": { + "type": "any", + "required": false + }, + "prompt_llm_params": { + "type": "any", + "required": false + }, + "pronunciations": { + "type": "list", + "required": false + } + } + }, + "signalwire.core.mixins.prompt_mixin.PromptMixin": { + "params": { + "post_prompt": { + "type": "string", + "required": false + }, + "prompt_pom": { + "type": "list", + "required": false + }, + "prompt_text": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.mixins.web_mixin.WebMixin": { + "params": { + "dynamic_config_callback": { + "type": "callable,dict,dict,class:signalwire.core.agent_base.AgentBase>,void>", + "required": false + } + } + }, + "signalwire.core.security.session_manager.SessionManager": { + "params": { + "debug_mode": { + "type": "bool", + "required": false + }, + "secret_key": { + "type": "string", + "required": false + }, + "token_expiry_secs": { + "type": "int", + "required": false + } + } + }, + "signalwire.core.security_config.SecurityConfig": { + "params": { + "config_file": { + "type": "optional", + "required": false + }, + "service_name": { + "type": "optional", + "required": false + } + } + }, + "signalwire.core.swaig_function.SWAIGFunction": { + "params": { + "description": { + "type": "string", + "required": true + }, + "extra_swaig_fields": { + "type": "any", + "required": false + }, + "fillers": { + "type": "optional", + "required": false + }, + "handler": { + "type": "class:signalwire.swaig_function_handler.SwaigFunctionHandler", + "required": true + }, + "is_typed_handler": { + "type": "bool", + "required": false + }, + "name": { + "type": "string", + "required": true + }, + "parameters": { + "type": "any", + "required": false + }, + "required": { + "type": "list", + "required": false + }, + "secure": { + "type": "bool", + "required": false + }, + "wait_file": { + "type": "optional", + "required": false + }, + "wait_file_loops": { + "type": "optional", + "required": false + }, + "webhook_url": { + "type": "optional", + "required": false + } + } + }, + "signalwire.core.swaig_function.ToolDefinition": { + "params": { + "description": { + "type": "string", + "required": false + }, + "handler": { + "type": "callable,class:signalwire.core.function_result.FunctionResult>", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "parameters": { + "type": "any", + "required": false + }, + "secure": { + "type": "bool", + "required": false + } + } + }, + "signalwire.core.swml_builder.SWMLBuilder": { + "params": { + "service": { + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true + } + } + }, + "signalwire.core.swml_service.SWMLService": { + "params": { + "basic_auth": { + "type": "optional>", + "required": false + }, + "config_file": { + "type": "optional", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "port": { + "type": "optional", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "schema_path": { + "type": "optional", + "required": false + }, + "schema_validation": { + "type": "bool", + "required": false + } + } + }, + "signalwire.logger.Logger": { + "params": { + "level": { + "type": "class:signalwire.log_level.LogLevel", + "required": false + } + } + }, + "signalwire.logging.logger.Logger": { + "params": { + "name": { + "type": "string", + "required": true + } + } + }, + "signalwire.pom.pom.PromptObjectModel": { + "params": { + "debug": { + "type": "bool", + "required": false + }, + "sections": { + "type": "list", + "required": false + } + } + }, + "signalwire.pom.pom.Section": { + "params": { + "body": { + "type": "string", + "required": false + }, + "bullets": { + "type": "list", + "required": false + }, + "numbered": { + "type": "optional", + "required": false + }, + "numberedBullets": { + "type": "bool", + "required": false + }, + "subsections": { + "type": "list", + "required": false + }, + "title": { + "type": "optional", + "required": false + } + } + }, + "signalwire.prefabs.concierge.ConciergeAgent": { + "params": { + "host": { + "type": "string", + "required": false + }, + "hours": { + "type": "any", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "route": { + "type": "string", + "required": false + } + } + }, + "signalwire.prefabs.faq_bot.FAQBotAgent": { + "params": { + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "no_match_message": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "route": { + "type": "string", + "required": false + } + } + }, + "signalwire.prefabs.info_gatherer.InfoGathererAgent": { + "params": { + "completion_message": { + "type": "string", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "prefix": { + "type": "string", + "required": false + }, + "question_callback": { + "type": "class:signalwire.question_callback.QuestionCallback", + "required": false + }, + "questions": { + "type": "list", + "required": false + }, + "route": { + "type": "string", + "required": false + } + } + }, + "signalwire.prefabs.receptionist.ReceptionistAgent": { + "params": { + "departments": { + "type": "any", + "required": false + }, + "greeting": { + "type": "string", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "route": { + "type": "string", + "required": false + }, + "transfer_message": { + "type": "string", + "required": false + } + } + }, + "signalwire.prefabs.survey.SurveyAgent": { + "params": { + "completion_message": { + "type": "string", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "intro_message": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "route": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.action.Action": { + "params": { + "event_type_filter": { + "type": "list", + "required": false + }, + "method_prefix": { + "type": "string", + "required": false + }, + "resolve_on_detect": { + "type": "bool", + "required": false + }, + "resolve_on_result": { + "type": "bool", + "required": false + } + } + }, + "signalwire.relay.call.Call": { + "params": { + "client": { + "type": "class:signalwire.relay.client.RelayClient", + "required": false + }, + "from": { + "type": "string", + "required": false + }, + "to": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.call_event.CallEvent": { + "params": { + "call_id": { + "type": "string", + "required": false + }, + "call_state": { + "type": "string", + "required": false + }, + "node_id": { + "type": "string", + "required": false + }, + "peer_call_id": { + "type": "optional", + "required": false + }, + "tag": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.client.RelayClient": { + "params": { + "contexts": { + "type": "list", + "required": false + }, + "host": { + "type": "string", + "required": false + }, + "jwt_token": { + "type": "string", + "required": false + }, + "max_active_calls": { + "type": "int", + "required": false + }, + "max_connections": { + "type": "int", + "required": false + }, + "port": { + "type": "int", + "required": false + }, + "project": { + "type": "string", + "required": false + }, + "request_timeout_ms": { + "type": "int", + "required": false + }, + "token": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.client.RelayError": { + "params": { + "code": { + "type": "int", + "required": true + }, + "message": { + "type": "string", + "required": true + } + } + }, + "signalwire.relay.component_event.ComponentEvent": { + "params": { + "call_id": { + "type": "string", + "required": false + }, + "control_id": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.device.Device": { + "params": { + "params": { + "type": "any", + "required": false + }, + "type": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.dial_event.DialEvent": { + "params": { + "call_info": { + "type": "any", + "required": false + }, + "dial_state": { + "type": "string", + "required": false + }, + "tag": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.CallReceiveEvent": { + "params": { + "call_state": { + "type": "string", + "required": false + }, + "context": { + "type": "string", + "required": false + }, + "device": { + "type": "any", + "required": false + }, + "direction": { + "type": "string", + "required": false + }, + "node_id": { + "type": "string", + "required": false + }, + "project_id": { + "type": "string", + "required": false + }, + "segment_id": { + "type": "string", + "required": false + }, + "tag": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.CallStateEvent": { + "params": { + "call_state": { + "type": "string", + "required": false + }, + "device": { + "type": "any", + "required": false + }, + "direction": { + "type": "string", + "required": false + }, + "end_reason": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.CallingErrorEvent": { + "params": { + "code": { + "type": "string", + "required": false + }, + "message": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.CollectEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "final": { + "type": "optional", + "required": false + }, + "result": { + "type": "any", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.ConferenceEvent": { + "params": { + "conference_id": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "status": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.ConnectEvent": { + "params": { + "connect_state": { + "type": "string", + "required": false + }, + "peer": { + "type": "any", + "required": false + } + } + }, + "signalwire.relay.event.DenoiseEvent": { + "params": { + "denoised": { + "type": "bool", + "required": false + } + } + }, + "signalwire.relay.event.DetectEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "detect": { + "type": "any", + "required": false + } + } + }, + "signalwire.relay.event.DialEvent": { + "params": { + "call": { + "type": "any", + "required": false + }, + "dial_state": { + "type": "string", + "required": false + }, + "tag": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.EchoEvent": { + "params": { + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.FaxEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "fax": { + "type": "any", + "required": false + } + } + }, + "signalwire.relay.event.HoldEvent": { + "params": { + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.MessageReceiveEvent": { + "params": { + "body": { + "type": "string", + "required": false + }, + "context": { + "type": "string", + "required": false + }, + "direction": { + "type": "string", + "required": false + }, + "from_number": { + "type": "string", + "required": false + }, + "media": { + "type": "any", + "required": false + }, + "message_id": { + "type": "string", + "required": false + }, + "message_state": { + "type": "string", + "required": false + }, + "segments": { + "type": "any", + "required": false + }, + "tags": { + "type": "any", + "required": false + }, + "to_number": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.MessageStateEvent": { + "params": { + "body": { + "type": "string", + "required": false + }, + "context": { + "type": "string", + "required": false + }, + "direction": { + "type": "string", + "required": false + }, + "from_number": { + "type": "string", + "required": false + }, + "media": { + "type": "any", + "required": false + }, + "message_id": { + "type": "string", + "required": false + }, + "message_state": { + "type": "string", + "required": false + }, + "reason": { + "type": "string", + "required": false + }, + "segments": { + "type": "any", + "required": false + }, + "tags": { + "type": "any", + "required": false + }, + "to_number": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.PayEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.PlayEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.QueueEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "position": { + "type": "int", + "required": false + }, + "queue_id": { + "type": "string", + "required": false + }, + "queue_name": { + "type": "string", + "required": false + }, + "size": { + "type": "int", + "required": false + }, + "status": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.RecordEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "duration": { + "type": "float", + "required": false + }, + "record": { + "type": "any", + "required": false + }, + "size": { + "type": "int", + "required": false + }, + "state": { + "type": "string", + "required": false + }, + "url": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.ReferEvent": { + "params": { + "sip_notify_response_code": { + "type": "string", + "required": false + }, + "sip_refer_response_code": { + "type": "string", + "required": false + }, + "sip_refer_to": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.RelayEvent": { + "params": { + "call_id": { + "type": "string", + "required": false + }, + "event_type": { + "type": "string", + "required": false + }, + "params": { + "type": "any", + "required": false + }, + "timestamp": { + "type": "float", + "required": false + } + } + }, + "signalwire.relay.event.SendDigitsEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.StreamEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "state": { + "type": "string", + "required": false + }, + "url": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.event.TapEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "device": { + "type": "any", + "required": false + }, + "state": { + "type": "string", + "required": false + }, + "tap": { + "type": "any", + "required": false + } + } + }, + "signalwire.relay.event.TranscribeEvent": { + "params": { + "control_id": { + "type": "string", + "required": false + }, + "duration": { + "type": "float", + "required": false + }, + "recording_id": { + "type": "string", + "required": false + }, + "size": { + "type": "int", + "required": false + }, + "state": { + "type": "string", + "required": false + }, + "url": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.message.Message": { + "params": { + "body": { + "type": "string", + "required": false + }, + "context": { + "type": "string", + "required": false + }, + "direction": { + "type": "string", + "required": false + }, + "from_number": { + "type": "string", + "required": false + }, + "media": { + "type": "list", + "required": false + }, + "message_id": { + "type": "string", + "required": false + }, + "region": { + "type": "string", + "required": false + }, + "segments": { + "type": "int", + "required": false + }, + "tags": { + "type": "list", + "required": false + }, + "to_number": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.message_event.MessageEvent": { + "params": { + "body": { + "type": "string", + "required": false + }, + "from": { + "type": "string", + "required": false + }, + "message_id": { + "type": "string", + "required": false + }, + "message_state": { + "type": "string", + "required": false + }, + "to": { + "type": "string", + "required": false + } + } + }, + "signalwire.relay.relay_event.RelayEvent": { + "params": { + "event_channel": { + "type": "string", + "required": false + }, + "event_type": { + "type": "string", + "required": false + }, + "params": { + "type": "any", + "required": false + }, + "timestamp": { + "type": "datetime", + "required": false + } + } + }, + "signalwire.rest._base.BaseResource": { + "params": { + "base_path": { + "type": "string", + "required": true + }, + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest._base.CrudResource": { + "params": { + "base_path": { + "type": "string", + "required": true + }, + "client": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + }, + "update_method": { + "type": "string", + "required": false + } + } + }, + "signalwire.rest._base.CrudWithAddresses": { + "params": { + "base_path": { + "type": "string", + "required": true + }, + "client": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + }, + "update_method": { + "type": "string", + "required": false + } + } + }, + "signalwire.rest._base.HttpClient": { + "params": { + "base_url": { + "type": "string", + "required": true + }, + "ca_cert_path": { + "type": "string", + "required": false + }, + "password": { + "type": "string", + "required": true + }, + "request_options": { + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false + }, + "timeout": { + "type": "int", + "required": false + }, + "username": { + "type": "string", + "required": true + } + } + }, + "signalwire.rest._base.ReadResource": { + "params": { + "base_path": { + "type": "string", + "required": true + }, + "client": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest._base.SignalWireRestError": { + "params": { + "body": { + "type": "string", + "required": false + }, + "headers": { + "type": "dict", + "required": false + }, + "message": { + "type": "string", + "required": true + }, + "method": { + "type": "string", + "required": false + }, + "status": { + "type": "int", + "required": true + }, + "url": { + "type": "string", + "required": false + } + } + }, + "signalwire.rest._base.SignalWireRestTransportError": { + "params": { + "message": { + "type": "string", + "required": true + }, + "method": { + "type": "string", + "required": false + }, + "url": { + "type": "string", + "required": false + } + } + }, + "signalwire.rest._pagination.PaginatedIterator": { + "params": { + "data_key": { + "type": "string", + "required": false + }, + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + }, + "params": { + "type": "dict", + "required": false + }, + "path": { + "type": "string", + "required": true + }, + "request_options": { + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false + } + } + }, + "signalwire.rest._request_options.RequestOptions": { + "params": { + "abort_signal": { + "type": "optional", + "required": false + }, + "retries": { + "type": "optional", + "required": false + }, + "retry_backoff": { + "type": "optional", + "required": false + }, + "retry_on_status": { + "type": "optional>", + "required": false + }, + "timeout": { + "type": "optional", + "required": false + } + } + }, + "signalwire.rest.client.RestClient": { + "params": { + "host": { + "type": "string", + "required": true + }, + "project": { + "type": "string", + "required": true + }, + "request_options": { + "type": "class:signalwire.rest._request_options.RequestOptions", + "required": false + }, + "token": { + "type": "string", + "required": true + } + } + }, + "signalwire.rest.generated.resource_tree.ResourceTree": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.DatasphereNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.FabricNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.LogsNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.ProjectNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.RegistryNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces._client_tree_generated.VideoNamespace": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.calling_resources_generated.Calling": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.chat_resources_generated.Chat": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.datasphere_resources_generated.DatasphereDocuments": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.AiAgents": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.CallFlows": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.ConferenceRooms": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.CxmlApplications": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.CxmlScripts": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.CxmlWebhooks": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.FabricAddresses": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.FabricTokens": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.FreeswitchConnectors": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.GenericResources": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.RelayApplications": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.SipEndpoints": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.SipGateways": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.Subscribers": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.SwmlScripts": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fabric_resources_generated.SwmlWebhooks": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.fax_resources_generated.FaxLogs": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.logs_resources_generated.ConferenceLogs": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.message_resources_generated.MessageLogs": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.messages_resources_generated.Messages": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.project_resources_generated.ProjectTokens": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.projects_resources_generated.Projects": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.pubsub_resources_generated.PubSub": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.Addresses": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.ImportedNumbers": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.Lookup": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.Mfa": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.NumberGroups": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.PhoneNumbers": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.Queues": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.Recordings": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.RegistryBrands": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.RegistryCampaigns": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.RegistryNumbers": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.RegistryOrders": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.ShortCodes": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.SipProfile": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.relay_rest_resources_generated.VerifiedCallers": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoConferenceTokens": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoConferences": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoRoomRecordings": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoRoomSessions": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoRoomTokens": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoRooms": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.video_resources_generated.VideoStreams": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.rest.namespaces.voice_resources_generated.VoiceLogs": { + "params": { + "http": { + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + } + }, + "signalwire.swml.document.Document": { + "params": { + "version": { + "type": "string", + "required": false + } + } + }, + "signalwire.swml.section.Section": { + "params": { + "name": { + "type": "string", + "required": false + }, + "verbs": { + "type": "list", + "required": false + } + } + }, + "signalwire.swml.verb.Verb": { + "params": { + "name": { + "type": "string", + "required": false + }, + "params": { + "type": "any", + "required": false + } + } + }, + "signalwire.utils.schema_utils.SchemaUtils": { + "params": { + "schema_path": { + "type": "string", + "required": false + }, + "schema_validation": { + "type": "bool", + "required": false + } + } + }, + "signalwire.utils.schema_utils.SchemaValidationError": { + "params": { + "errors": { + "type": "list", + "required": true + }, + "verb_name": { + "type": "string", + "required": true + } + } + }, + "signalwire.web.web_service.WebService": { + "params": { + "allowed_extensions": { + "type": "optional>", + "required": false + }, + "basic_auth": { + "type": "optional>", + "required": false + }, + "blocked_extensions": { + "type": "optional>", + "required": false + }, + "config_file": { + "type": "optional", + "required": false + }, + "directories": { + "type": "optional>", + "required": false + }, + "enable_cors": { + "type": "bool", + "required": false + }, + "enable_directory_browsing": { + "type": "bool", + "required": false + }, + "max_file_size": { + "type": "int", + "required": false + }, + "port": { + "type": "int", + "required": false + } + } + } + } } diff --git a/port_signatures.json b/port_signatures.json index 101f42b..b703ef1 100644 --- a/port_signatures.json +++ b/port_signatures.json @@ -10,13 +10,13 @@ "name": "args", "type": "list", "required": false, - "default": null + "default": [] }, { "name": "kwargs", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "class:signalwire.rest.client.RestClient" @@ -113,19 +113,19 @@ "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 }, { "name": "log_level", "type": "string", "required": false, - "default": null + "default": "info" } ], "returns": "void" @@ -140,7 +140,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -347,7 +347,7 @@ "name": "route", "type": "string", "required": false, - "default": null + "default": "/" } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -376,13 +376,13 @@ "name": "route", "type": "string", "required": false, - "default": null + "default": "/sip" }, { "name": "auto_map", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.agent_server.AgentServer" @@ -442,13 +442,13 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "bedrock_agent" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/bedrock" }, { "name": "system_prompt", @@ -460,25 +460,25 @@ "name": "voice_id", "type": "string", "required": false, - "default": null + "default": "matthew" }, { "name": "temperature", "type": "float", "required": false, - "default": null + "default": 0.7 }, { "name": "top_p", "type": "float", "required": false, - "default": null + "default": 0.9 }, { "name": "max_tokens", "type": "int", "required": false, - "default": null + "default": 1024 } ], "returns": "void" @@ -1018,19 +1018,19 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "agent" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", @@ -1048,37 +1048,37 @@ "name": "use_pom", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "token_expiry_secs", "type": "int", "required": false, - "default": null + "default": 3600 }, { "name": "auto_answer", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "record_call", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "record_format", "type": "string", "required": false, - "default": null + "default": "mp4" }, { "name": "record_stereo", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "default_webhook_url", @@ -1108,19 +1108,19 @@ "name": "suppress_logs", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "enable_post_prompt_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "check_for_input_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "config_file", @@ -1132,7 +1132,7 @@ "name": "schema_validation", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "signing_key", @@ -1144,7 +1144,7 @@ "name": "trust_proxy_for_signature", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "void" @@ -1218,13 +1218,13 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", "type": "list", "required": false, - "default": null + "default": [] } ], "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" @@ -1249,11 +1249,11 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", - "type": "list", + "type": "optional>", "required": false, "default": null } @@ -1281,7 +1281,7 @@ "name": "bullets", "type": "list", "required": false, - "default": null + "default": [] } ], "returns": "class:signalwire.core.agent.prompt.manager.PromptManager" @@ -1360,19 +1360,19 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "agent" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", @@ -1390,37 +1390,37 @@ "name": "use_pom", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "token_expiry_secs", "type": "int", "required": false, - "default": null + "default": 3600 }, { "name": "auto_answer", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "record_call", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "record_format", "type": "string", "required": false, - "default": null + "default": "mp4" }, { "name": "record_stereo", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "default_webhook_url", @@ -1450,19 +1450,19 @@ "name": "suppress_logs", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "enable_post_prompt_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "check_for_input_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "config_file", @@ -1474,7 +1474,7 @@ "name": "schema_validation", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "signing_key", @@ -1486,7 +1486,7 @@ "name": "trust_proxy_for_signature", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "void" @@ -1632,19 +1632,19 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "agent" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", @@ -1662,37 +1662,37 @@ "name": "use_pom", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "token_expiry_secs", "type": "int", "required": false, - "default": null + "default": 3600 }, { "name": "auto_answer", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "record_call", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "record_format", "type": "string", "required": false, - "default": null + "default": "mp4" }, { "name": "record_stereo", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "default_webhook_url", @@ -1722,19 +1722,19 @@ "name": "suppress_logs", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "enable_post_prompt_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "check_for_input_override", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "config_file", @@ -1746,7 +1746,7 @@ "name": "schema_validation", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "signing_key", @@ -1758,7 +1758,7 @@ "name": "trust_proxy_for_signature", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "void" @@ -1923,7 +1923,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -2002,7 +2002,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -2017,7 +2017,7 @@ "name": "include_auth", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "string" @@ -2456,7 +2456,7 @@ "name": "optional", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "callable>,class:signalwire.core.auth_result.AuthResult>" @@ -2492,7 +2492,7 @@ }, { "name": "credentials", - "type": "class:signalwire.core.basic_credentials.BasicCredentials", + "type": "class:signalwire.core.auth_handler.BasicCredentials", "required": true } ], @@ -2506,13 +2506,75 @@ }, { "name": "credentials", - "type": "class:signalwire.core.bearer_credentials.BearerCredentials", + "type": "class:signalwire.core.auth_handler.BearerCredentials", "required": true } ], "returns": "bool" } } + }, + "BasicCredentials": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "BearerCredentials": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "credentials": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "scheme": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } } } }, @@ -2632,7 +2694,7 @@ "name": "env_prefix", "type": "string", "required": false, - "default": null + "default": "SWML_" } ], "returns": "any" @@ -2652,7 +2714,7 @@ "name": "max_depth", "type": "int", "required": false, - "default": null + "default": 10 } ], "returns": "any" @@ -3213,19 +3275,19 @@ }, { "name": "output_key", - "type": "string", + "type": "optional", "required": false, "default": null }, { "name": "completion_action", - "type": "string", + "type": "optional", "required": false, "default": null }, { "name": "prompt", - "type": "string", + "type": "optional", "required": false, "default": null }, @@ -3233,7 +3295,7 @@ "name": "isolated", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "void" @@ -3258,17 +3320,17 @@ "name": "type", "type": "string", "required": false, - "default": null + "default": "string" }, { "name": "confirm", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "prompt", - "type": "string", + "type": "optional", "required": false, "default": null }, @@ -3294,7 +3356,7 @@ "kind": "self" } ], - "returns": "string" + "returns": "optional" }, "has_questions": { "params": [ @@ -3347,17 +3409,17 @@ "name": "type", "type": "string", "required": false, - "default": null + "default": "string" }, { "name": "confirm", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "prompt", - "type": "string", + "type": "optional", "required": false, "default": null }, @@ -3365,7 +3427,7 @@ "name": "functions", "type": "list", "required": false, - "default": null + "default": [] }, { "name": "isolated", @@ -3419,7 +3481,7 @@ "kind": "self" } ], - "returns": "string" + "returns": "optional" }, "question": { "params": [ @@ -3500,17 +3562,17 @@ "name": "type", "type": "string", "required": false, - "default": null + "default": "string" }, { "name": "confirm", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "prompt", - "type": "string", + "type": "optional", "required": false, "default": null }, @@ -3620,19 +3682,19 @@ }, { "name": "output_key", - "type": "string", + "type": "optional", "required": false, "default": null }, { "name": "completion_action", - "type": "string", + "type": "optional", "required": false, "default": null }, { "name": "prompt", - "type": "string", + "type": "optional", "required": false, "default": null }, @@ -3640,7 +3702,7 @@ "name": "isolated", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.contexts.Step" @@ -3848,20 +3910,6 @@ ], "returns": "void" }, - "body": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "data", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.core.data_map.DataMap" - }, "description": { "params": [ { @@ -4010,7 +4058,7 @@ "name": "required", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "enum_values", @@ -4090,7 +4138,7 @@ "name": "input_args_as_params", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "require_args", @@ -4133,13 +4181,13 @@ "name": "response", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "post_process", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "void" @@ -4215,7 +4263,7 @@ "name": "final", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "from_addr", @@ -4293,7 +4341,7 @@ "name": "enabled", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4308,7 +4356,7 @@ "name": "enabled", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4360,7 +4408,7 @@ "name": "transfer", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4384,7 +4432,7 @@ "name": "timeout", "type": "int", "required": false, - "default": null + "default": 300 } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4404,25 +4452,25 @@ "name": "muted", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "beep", "type": "string", "required": false, - "default": null + "default": "true" }, { "name": "start_on_enter", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "end_on_exit", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "wait_url", @@ -4434,13 +4482,13 @@ "name": "max_participants", "type": "int", "required": false, - "default": null + "default": 250 }, { "name": "record", "type": "string", "required": false, - "default": null + "default": "do-not-record" }, { "name": "region", @@ -4452,7 +4500,7 @@ "name": "trim", "type": "string", "required": false, - "default": null + "default": "trim-silence" }, { "name": "coach", @@ -4476,7 +4524,7 @@ "name": "status_callback_method", "type": "string", "required": false, - "default": null + "default": "POST" }, { "name": "recording_status_callback", @@ -4488,13 +4536,13 @@ "name": "recording_status_callback_method", "type": "string", "required": false, - "default": null + "default": "POST" }, { "name": "recording_status_callback_event", "type": "string", "required": false, - "default": null + "default": "completed" }, { "name": "result", @@ -4534,7 +4582,7 @@ "name": "input_method", "type": "string", "required": false, - "default": null + "default": "dtmf" }, { "name": "status_url", @@ -4546,43 +4594,43 @@ "name": "payment_method", "type": "string", "required": false, - "default": null + "default": "credit-card" }, { "name": "timeout", "type": "int", "required": false, - "default": null + "default": 5 }, { "name": "max_attempts", "type": "int", "required": false, - "default": null + "default": 1 }, { "name": "security_code", "type": "bool", "required": false, - "default": null + "default": true }, { "name": "postal_code", - "type": "string", + "type": "union", "required": false, - "default": null + "default": true }, { "name": "min_postal_code_length", "type": "int", "required": false, - "default": null + "default": 0 }, { "name": "token_type", "type": "string", "required": false, - "default": null + "default": "reusable" }, { "name": "charge_amount", @@ -4594,19 +4642,19 @@ "name": "currency", "type": "string", "required": false, - "default": null + "default": "usd" }, { "name": "language", "type": "string", "required": false, - "default": null + "default": "en-US" }, { "name": "voice", "type": "string", "required": false, - "default": null + "default": "woman" }, { "name": "description", @@ -4618,7 +4666,7 @@ "name": "valid_card_types", "type": "string", "required": false, - "default": null + "default": "visa mastercard amex" }, { "name": "parameters", @@ -4636,7 +4684,7 @@ "name": "ai_response", "type": "string", "required": false, - "default": null + "default": "The payment status is ${pay_result}, do not mention anything else about collecting payment if successful." } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4656,7 +4704,7 @@ "name": "wait", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4679,22 +4727,26 @@ { "name": "control_id", "type": "string", - "required": true + "required": false, + "default": null }, { "name": "stereo", "type": "bool", - "required": true + "required": false, + "default": false }, { "name": "format", "type": "class:signalwire.swaig.record_format.RecordFormat", - "required": true + "required": false, + "default": "wav" }, { "name": "direction", "type": "class:signalwire.swaig.record_direction.RecordDirection", - "required": true + "required": false, + "default": "both" }, { "name": "terminators", @@ -4706,13 +4758,13 @@ "name": "beep", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "input_sensitivity", "type": "float", "required": false, - "default": null + "default": 44.0 }, { "name": "initial_timeout", @@ -4778,7 +4830,8 @@ { "name": "text", "type": "any", - "required": true + "required": false, + "default": true } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4812,7 +4865,7 @@ "name": "role", "type": "string", "required": false, - "default": null + "default": "system" } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -4856,7 +4909,7 @@ "name": "device_type", "type": "string", "required": false, - "default": null + "default": "phone" } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -5086,13 +5139,13 @@ "name": "consolidate", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "full_reset", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -5145,7 +5198,7 @@ "name": "final", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -5178,23 +5231,26 @@ { "name": "control_id", "type": "string", - "required": true + "required": false, + "default": null }, { "name": "direction", "type": "class:signalwire.swaig.tap_direction.TapDirection", - "required": true + "required": false, + "default": "both" }, { "name": "codec", "type": "class:signalwire.swaig.codec.Codec", - "required": true + "required": false, + "default": "PCMU" }, { "name": "rtp_ptime", "type": "int", "required": false, - "default": null + "default": 20 }, { "name": "status_url", @@ -5224,7 +5280,7 @@ "name": "indent", "type": "int", "required": false, - "default": null + "default": -1 } ], "returns": "string" @@ -5293,7 +5349,7 @@ "name": "answer_first", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -5320,7 +5376,7 @@ "required": true } ], - "returns": "bool" + "returns": "class:signalwire.logging.logger.Logger" }, "reset_logging_configuration": { "params": [], @@ -5329,12 +5385,12 @@ "strip_control_chars": { "params": [ { - "name": "value", - "type": "string", + "name": "event_dict", + "type": "any", "required": true } ], - "returns": "string" + "returns": "any" } } }, @@ -5438,7 +5494,7 @@ "name": "resources", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "resource_vars", @@ -5474,7 +5530,7 @@ "name": "ignore_case", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -5499,7 +5555,7 @@ "name": "ignore_case", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -5511,10 +5567,10 @@ "kind": "self" }, { - "name": "enable", - "type": "bool", + "name": "level", + "type": "int", "required": false, - "default": null + "default": 1 } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -5529,7 +5585,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -5837,13 +5893,13 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", "type": "list", "required": false, - "default": null + "default": [] } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -5868,11 +5924,11 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", - "type": "list", + "type": "optional>", "required": false, "default": null } @@ -5900,7 +5956,7 @@ "name": "bullets", "type": "list", "required": false, - "default": null + "default": [] } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -6157,7 +6213,8 @@ { "name": "raw_data", "type": "any", - "required": true + "required": false, + "default": null } ], "returns": "class:signalwire.core.function_result.FunctionResult" @@ -6203,7 +6260,7 @@ "name": "enable", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -6260,6 +6317,12 @@ "type": "optional", "required": false, "default": null + }, + { + "name": "request", + "type": "optional", + "required": false, + "default": null } ], "returns": "optional" @@ -6279,7 +6342,7 @@ "name": "path", "type": "string", "required": false, - "default": null + "default": "/sip" } ], "returns": "class:signalwire.core.agent_base.AgentBase" @@ -6357,7 +6420,7 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", @@ -6369,13 +6432,13 @@ "name": "numbered", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "numbered_bullets", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "subsections", @@ -6406,7 +6469,7 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", @@ -6540,16 +6603,16 @@ "classes": { "PostPrompt": { "methods": { - "__init__": { + "SWMLCall": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "call_log": { + "SWMLVars": { "params": [ { "name": "self", @@ -6558,16 +6621,16 @@ ], "returns": "any" }, - "post_prompt_data": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "raw_call_log": { + "action": { "params": [ { "name": "self", @@ -6576,7 +6639,7 @@ ], "returns": "any" }, - "swaig_log": { + "ai_end_date": { "params": [ { "name": "self", @@ -6585,7 +6648,7 @@ ], "returns": "any" }, - "times": { + "ai_id_tag": { "params": [ { "name": "self", @@ -6593,21 +6656,17 @@ } ], "returns": "any" - } - } - }, - "PostPromptAssistantEntry": { - "methods": { - "__init__": { + }, + "ai_session_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "stamps_us": { + "ai_start_date": { "params": [ { "name": "self", @@ -6615,21 +6674,17 @@ } ], "returns": "any" - } - } - }, - "PostPromptSwaigLogEntry": { - "methods": { - "__init__": { + }, + "app_name": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "post_data": { + "call_answer_date": { "params": [ { "name": "self", @@ -6637,21 +6692,17 @@ } ], "returns": "any" - } - } - }, - "PostPromptUserEntry": { - "methods": { - "__init__": { + }, + "call_end_date": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "entity": { + "call_ended_by": { "params": [ { "name": "self", @@ -6660,7 +6711,7 @@ ], "returns": "any" }, - "eot": { + "call_id": { "params": [ { "name": "self", @@ -6669,7 +6720,7 @@ ], "returns": "any" }, - "timing": { + "call_log": { "params": [ { "name": "self", @@ -6677,496 +6728,215 @@ } ], "returns": "any" - } - } - } - } - }, - "signalwire.core.security.security_utils": { - "functions": { - "filter_sensitive_headers": { - "params": [ - { - "name": "headers", - "type": "dict", - "required": true - } - ], - "returns": "dict" - }, - "is_valid_hostname": { - "params": [ - { - "name": "host", - "type": "string", - "required": true - } - ], - "returns": "bool" - }, - "redact_url": { - "params": [ - { - "name": "url", - "type": "string", - "required": true - } - ], - "returns": "string" - } - } - }, - "signalwire.core.security.session_manager": { - "classes": { - "SessionManager": { - "methods": { - "__init__": { + }, + "call_start_date": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "token_expiry_secs", - "type": "int", - "required": false, - "default": null - }, - { - "name": "secret_key", - "type": "string", - "required": false, - "default": null } ], - "returns": "void" + "returns": "any" }, - "activate_session": { + "call_timeline": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "create_session": { + "caller_id_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": false, - "default": null } ], - "returns": "string" + "returns": "any" }, - "create_token": { + "caller_id_number": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "call_id", - "type": "string", - "required": true - }, - { - "name": "expiry_seconds", - "type": "int", - "required": false, - "default": null } ], - "returns": "string" + "returns": "any" }, - "create_tool_token": { + "content_disposition": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "string" + "returns": "any" }, - "debug_token": { + "content_type": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "token", - "type": "string", - "required": true } ], "returns": "any" }, - "end_session": { + "conversation_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "generate_token": { + "conversation_summary": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "string" + "returns": "any" }, - "get_session_metadata": { + "conversation_type": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true } ], "returns": "any" }, - "secret_key": { + "global_data": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "set_debug_mode": { + "hard_timeout": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "enabled", - "type": "bool", - "required": true } ], - "returns": "void" + "returns": "any" }, - "set_session_metadata": { + "post_prompt_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "call_id", - "type": "string", - "required": true - }, - { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "value", - "type": "any", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "timing_safe_compare": { + "previous_contexts": { "params": [ { - "name": "a", - "type": "string", - "required": true - }, - { - "name": "b", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "token_expiry_secs": { + "project_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "any" }, - "validate_token": { + "raw_call_log": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "token", - "type": "string", - "required": true - }, - { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "validate_tool_token": { + "space_id": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "function_name", - "type": "string", - "required": true - }, - { - "name": "token", - "type": "string", - "required": true - }, - { - "name": "call_id", - "type": "string", - "required": true } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.security.webhook_middleware": { - "functions": { - "validate": { - "params": [ - { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "headers", - "type": "dict", - "required": true - }, - { - "name": "body", - "type": "string", - "required": true - }, - { - "name": "signing_key", - "type": "string", - "required": true, - "kind": "keyword" - } - ], - "returns": "optional,string>>" - } - } - }, - "signalwire.core.security.webhook_validator": { - "functions": { - "validate_request": { - "params": [ - { - "name": "signing_key", - "type": "string", - "required": true - }, - { - "name": "signature", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "params_or_raw_body", - "type": "union>>>", - "required": true - } - ], - "returns": "bool" - }, - "validate_webhook_signature": { - "params": [ - { - "name": "signing_key", - "type": "string", - "required": true - }, - { - "name": "signature", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true + "returns": "any" }, - { - "name": "raw_body", - "type": "string", - "required": true - } - ], - "returns": "bool" - } - } - }, - "signalwire.core.security_config": { - "classes": { - "SecurityConfig": { - "methods": { - "__init__": { + "swaig_log": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "config_file", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "service_name", - "type": "optional", - "required": false, - "default": null } ], - "returns": "void" + "returns": "any" }, - "allowed_hosts": { + "times": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "basic_auth_password": { + "total_asr_cost_factor": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "basic_auth_user": { + "total_asr_minutes": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "cors_origins": { + "total_input_tokens": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "domain": { + "total_minutes": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "get_basic_auth": { + "total_output_tokens": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "tuple" + "returns": "any" }, - "get_cors_config": { + "total_tts_chars": { "params": [ { "name": "self", @@ -7175,22 +6945,16 @@ ], "returns": "any" }, - "get_security_headers": { + "total_tts_chars_per_min": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "is_https", - "type": "bool", - "required": false, - "default": null } ], "returns": "any" }, - "get_ssl_context_kwargs": { + "total_wire_input_tokens": { "params": [ { "name": "self", @@ -7199,222 +6963,182 @@ ], "returns": "any" }, - "get_url_scheme": { + "total_wire_input_tokens_per_minute": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "hsts_max_age": { + "total_wire_output_tokens": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "any" }, - "load_from_env": { + "total_wire_output_tokens_per_minute": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" - }, - "log_config": { + "returns": "any" + } + } + }, + "PostPromptAssistantEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "service_name", - "type": "string", - "required": true } ], "returns": "void" }, - "max_request_size": { + "acoustic_latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "any" }, - "rate_limit": { + "audio_latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "any" }, - "request_timeout": { + "barge_elapsed_ms": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" + "returns": "any" }, - "should_allow_host": { + "barged": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "host", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "ssl_cert_path": { + "content": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "ssl_enabled": { + "dg_decision_latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "ssl_key_path": { + "eos_to_push_latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "ssl_verify_mode": { + "last_word_end_wall_us": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "use_hsts": { + "latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "validate_ssl_config": { + "poll": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.ssl_validation_result.SslValidationResult" - } - } - } - } - }, - "signalwire.core.skill_base": { - "classes": { - "SkillBase": { - "methods": { - "__init__": { + "returns": "any" + }, + "role": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "agent": { + "speech_start_wall_us": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "cleanup": { + "stamps_us": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "define_tool": { + "status_pushed_wall_us": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "description", - "type": "string", - "required": true - }, - { - "name": "parameters", - "type": "any", - "required": true - }, - { - "name": "handler", - "type": "callable,class:signalwire.core.function_result.FunctionResult>", - "required": true - }, - { - "name": "secure", - "type": "bool", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swaig_function.ToolDefinition" + "returns": "any" }, - "get_datamap_functions": { + "text_heard_approx": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "get_global_data": { + "text_spoken_total": { "params": [ { "name": "self", @@ -7423,55 +7147,34 @@ ], "returns": "any" }, - "get_hints": { + "timestamp": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "get_instance_key": { + "tool_calls": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "get_param_or_env": { + "turn_decided_wall_us": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "key", - "type": "string", - "required": true - }, - { - "name": "env_var", - "type": "string", - "required": true - }, - { - "name": "default_val", - "type": "string", - "required": false, - "default": null } ], - "returns": "string" + "returns": "any" }, - "get_parameter_schema": { + "utterance_latency": { "params": [ { "name": "self", @@ -7479,31 +7182,30 @@ } ], "returns": "any" - }, - "get_prompt_sections": { + } + } + }, + "PostPromptData": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "get_skill_data": { + "parsed": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], "returns": "any" }, - "params": { + "raw": { "params": [ { "name": "self", @@ -7512,389 +7214,248 @@ ], "returns": "any" }, - "register_tools": { + "substituted": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" - }, - "required_env_vars": { + "returns": "any" + } + } + }, + "PostPromptEntity": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "required_packages": { + "type": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "setup": { + "valid": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "skill_description": { + "value": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "skill_name": { + "returns": "any" + } + } + }, + "PostPromptEot": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "skill_version": { + "basis": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "supports_multiple_instances": { + "confidence": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" - }, - "update_skill_data": { + "returns": "any" + } + } + }, + "PostPromptStampsUs": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "result", - "type": "class:signalwire.core.function_result.FunctionResult", - "required": true - }, - { - "name": "data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "validate_env_vars": { + "first_audio": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "validate_packages": { + "first_token": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" - } - } - } - } - }, - "signalwire.core.skill_manager": { - "classes": { - "SkillManager": { - "methods": { - "__init__": { + "returns": "any" + }, + "first_utterance": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "agent": { + "last_word_end": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.agent_base.AgentBase" + "returns": "any" }, - "cleanup_all": { + "request_detect": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "get_skill": { + "speech_start": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.skill_base.SkillBase" + "returns": "any" }, - "has_skill": { + "status_pushed": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "is_loaded": { + "suspected_end": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "list_loaded": { + "turn_decided": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" - }, - "list_loaded_skills": { + "returns": "any" + } + } + }, + "PostPromptSwaigLogEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "load_skill": { + "active_count": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true - }, - { - "name": "params", - "type": "any", - "required": true - }, - { - "name": "agent", - "type": "class:signalwire.core.agent_base.AgentBase", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "unload_skill": { + "command_arg": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "skill_name", - "type": "string", - "required": true } ], - "returns": "void" - } - } - } - } - }, - "signalwire.core.swaig_function": { - "classes": { - "SWAIGFunction": { - "methods": { - "__init__": { + "returns": "any" + }, + "command_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "handler", - "type": "class:signalwire.swaig_function_handler.SwaigFunctionHandler", - "required": true - }, - { - "name": "description", - "type": "string", - "required": true - }, - { - "name": "parameters", - "type": "any", - "required": false, - "default": null - }, - { - "name": "secure", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "fillers", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "wait_file", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "wait_file_loops", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "webhook_url", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "required", - "type": "list", - "required": false, - "default": null - }, - { - "name": "is_typed_handler", - "type": "bool", - "required": false, - "default": null - }, - { - "name": "extra_swaig_fields", - "type": "any", - "required": false, - "default": null } ], - "returns": "void" + "returns": "any" }, - "call": { + "delayed_post_response": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": false, - "default": null } ], "returns": "any" }, - "description": { + "epoch_time": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "execute": { + "mcp_error": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "optional", - "required": false, - "default": null } ], "returns": "any" }, - "extra_swaig_fields": { + "mcp_response": { "params": [ { "name": "self", @@ -7903,52 +7464,52 @@ ], "returns": "any" }, - "fillers": { + "mcp_tool": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "handler": { + "mcp_url": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.swaig_function_handler.SwaigFunctionHandler" + "returns": "any" }, - "is_external": { + "native": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "is_typed_handler": { + "post_data": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "name": { + "post_response": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "parameters": { + "url": { "params": [ { "name": "self", @@ -7956,144 +7517,124 @@ } ], "returns": "any" - }, - "required": { + } + } + }, + "PostPromptSystemEntry": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "void" }, - "secure": { + "content": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "to_swaig": { + "role": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "base_url", - "type": "string", - "required": true - }, - { - "name": "token", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "call_id", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "include_auth", - "type": "bool", - "required": false, - "default": null } ], "returns": "any" }, - "validate_args": { + "timestamp": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + } + } + }, + "PostPromptSystemLogEntry": { + "methods": { + "__init__": { + "params": [ { - "name": "args", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.args_validation_result.ArgsValidationResult" + "returns": "void" }, - "wait_file": { + "action": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "wait_file_loops": { + "content": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" + "returns": "any" }, - "webhook_url": { + "content_type": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "optional" - } - } - }, - "ToolDefinition": { - "methods": { - "__init__": { + "returns": "any" + }, + "lang": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "to_swaig_json": { + "metadata": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "role": { + "params": [ { - "name": "web_hook_url", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], "returns": "any" - } - } - } - } - }, - "signalwire.core.swaig_request_generated": { - "classes": { - "SwaigRequest": { - "methods": { - "__init__": { + }, + "timestamp": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "argument": { + "tokens": { "params": [ { "name": "self", @@ -8103,108 +7644,46 @@ "returns": "any" } } - } - } - }, - "signalwire.core.swml_builder": { - "classes": { - "SWMLBuilder": { + }, + "PostPromptThinkingEntry": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", - "required": true } ], "returns": "void" }, - "add_section": { + "content": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "section_name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "ai": { + "lang": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "prompt_text", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "prompt_pom", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "post_prompt", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "post_prompt_url", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "swaig", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "kwargs", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "answer": { + "role": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "max_duration", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "codecs", - "type": "optional", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "build": { + "timestamp": { "params": [ { "name": "self", @@ -8213,144 +7692,94 @@ ], "returns": "any" }, - "hangup": { + "tokens": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + } + } + }, + "PostPromptTimesEntry": { + "methods": { + "__init__": { + "params": [ { - "name": "reason", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "void" }, - "play": { + "answer_time": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "url", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "urls", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "volume", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "say_voice", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "say_language", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "say_gender", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "auto_answer", - "type": "optional", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "render": { + "avg_tps": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "reset": { + "response": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "say": { + "response_word_count": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "text", - "type": "string", - "required": true - }, - { - "name": "voice", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "language", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "any" + }, + "token_time": { + "params": [ { - "name": "gender", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "tokens": { + "params": [ { - "name": "volume", - "type": "optional", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + "returns": "any" }, - "service": { + "tps": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" } } - } - } - }, - "signalwire.core.swml_handler": { - "classes": { - "AIVerbHandler": { + }, + "PostPromptTiming": { "methods": { "__init__": { "params": [ @@ -8361,47 +7790,45 @@ ], "returns": "void" }, - "build_config": { + "commit_latency_ms": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "kwargs", - "type": "any", - "required": false, - "default": null } ], "returns": "any" }, - "get_verb_name": { + "hold_ms": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "validate_config": { + "segments": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "walkbacks": { + "params": [ { - "name": "config", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + "returns": "any" } } }, - "SWMLVerbHandler": { + "PostPromptToolEntry": { "methods": { "__init__": { "params": [ @@ -8412,546 +7839,7747 @@ ], "returns": "void" }, - "build_config": { + "audio_latency": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "kwargs", - "type": "any", - "required": false, - "default": null } ], "returns": "any" }, - "get_verb_name": { + "content": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "validate_config": { + "deprecation_warning": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "config", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" - } - } - }, - "VerbHandlerRegistry": { - "methods": { - "__init__": { + "returns": "any" + }, + "distilled": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "get_handler": { + "end_timestamp": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.swml_handler.SWMLVerbHandler" + "returns": "any" }, - "get_verb_names": { + "execution_latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "has_handler": { + "function_latency": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "register_handler": { + "function_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "handler", - "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", - "required": true } ], - "returns": "void" - } - } - } - } - }, - "signalwire.core.swml_renderer": { - "classes": { - "SwmlRenderer": { - "methods": { - "__init__": { + "returns": "any" + }, + "latency": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "render_function_response_swml": { + "original_result": { "params": [ { - "name": "response_text", - "type": "string", - "required": true - }, - { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "role": { + "params": [ { - "name": "actions", - "type": "optional>", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "start_timestamp": { + "params": [ { - "name": "format", - "type": "string", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "render_swml": { + "timestamp": { "params": [ { - "name": "prompt", - "type": "any", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "tool_call_id": { + "params": [ { - "name": "service", - "type": "class:signalwire.core.swml_service.SWMLService", - "required": true - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "utterance_latency": { + "params": [ { - "name": "opts", - "type": "class:signalwire.core.render_options.RenderOptions", - "required": false, - "default": null + "name": "self", + "kind": "self" } ], - "returns": "string" + "returns": "any" } } - } - } - }, - "signalwire.core.swml_service": { - "classes": { - "SWMLService": { + }, + "PostPromptUserEntry": { "methods": { "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": false, - "default": null - }, - { - "name": "route", - "type": "string", - "required": false, - "default": null - }, - { - "name": "host", - "type": "string", - "required": false, - "default": null - }, - { - "name": "port", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "basic_auth", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "schema_path", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "config_file", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "schema_validation", - "type": "bool", - "required": false, - "default": null } ], "returns": "void" }, - "add_section": { + "barge_count": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "section_name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" }, - "add_verb": { + "confidence": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "config", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "add_verb_to_section": { + "content": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "section_name", - "type": "string", - "required": true - }, - { - "name": "verb_name", - "type": "string", - "required": true - }, - { - "name": "config", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "ai": { + "content_type": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "amazon_bedrock": { + "end_timestamp": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "answer": { + "entity": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "as_router": { + "eot": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.core.web.HostAppRouter" + "returns": "any" }, - "auth_password": { + "merge_count": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "auth_username": { + "merged": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "build_tool_registry_json": { + "role": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "any" }, - "cond": { + "speaker": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "connect": { + "speaking_to_final_event": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "define_tool": { + "speaking_to_turn_detection": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "tool", - "type": "class:signalwire.core.swaig_function.ToolDefinition", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "denoise": { + "start_timestamp": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" + }, + "timestamp": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timing": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "turn_detection_to_final_event": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.core.security.security_utils": { + "functions": { + "filter_sensitive_headers": { + "params": [ + { + "name": "headers", + "type": "dict", + "required": true + } + ], + "returns": "dict" + }, + "is_valid_hostname": { + "params": [ + { + "name": "host", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "redact_url": { + "params": [ + { + "name": "url", + "type": "string", + "required": true + } + ], + "returns": "string" + } + } + }, + "signalwire.core.security.session_manager": { + "classes": { + "SessionManager": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "token_expiry_secs", + "type": "int", + "required": false, + "default": 900 + }, + { + "name": "secret_key", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "void" + }, + "activate_session": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "create_session": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": false, + "default": null + } + ], + "returns": "string" + }, + "create_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + }, + { + "name": "expiry_seconds", + "type": "int", + "required": false, + "default": 3600 + } + ], + "returns": "string" + }, + "create_tool_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "string" + }, + "debug_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "token", + "type": "string", + "required": true + } + ], + "returns": "any" + }, + "end_session": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "generate_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "string" + }, + "get_session_metadata": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "any" + }, + "secret_key": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "set_debug_mode": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "enabled", + "type": "bool", + "required": true + } + ], + "returns": "void" + }, + "set_session_metadata": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "call_id", + "type": "string", + "required": true + }, + { + "name": "key", + "type": "string", + "required": true + }, + { + "name": "value", + "type": "any", + "required": true + } + ], + "returns": "bool" + }, + "timing_safe_compare": { + "params": [ + { + "name": "a", + "type": "string", + "required": true + }, + { + "name": "b", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "token_expiry_secs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "validate_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "token", + "type": "string", + "required": true + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "validate_tool_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "function_name", + "type": "string", + "required": true + }, + { + "name": "token", + "type": "string", + "required": true + }, + { + "name": "call_id", + "type": "string", + "required": true + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.core.security.webhook_middleware": { + "functions": { + "validate": { + "params": [ + { + "name": "method", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true + }, + { + "name": "body", + "type": "string", + "required": true + }, + { + "name": "signing_key", + "type": "string", + "required": true, + "kind": "keyword" + } + ], + "returns": "optional,string>>" + } + } + }, + "signalwire.core.security.webhook_validator": { + "functions": { + "validate_request": { + "params": [ + { + "name": "signing_key", + "type": "string", + "required": true + }, + { + "name": "signature", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "params_or_raw_body", + "type": "union>>>", + "required": true + } + ], + "returns": "bool" + }, + "validate_webhook_signature": { + "params": [ + { + "name": "signing_key", + "type": "string", + "required": true + }, + { + "name": "signature", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "raw_body", + "type": "string", + "required": true + } + ], + "returns": "bool" + } + } + }, + "signalwire.core.security_config": { + "classes": { + "SecurityConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "service_name", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "void" + }, + "allowed_hosts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "basic_auth_password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "basic_auth_user": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "cors_origins": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "domain": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "get_basic_auth": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "tuple" + }, + "get_cors_config": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_security_headers": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "is_https", + "type": "bool", + "required": false, + "default": false + } + ], + "returns": "any" + }, + "get_ssl_context_kwargs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_url_scheme": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "hsts_max_age": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "load_from_env": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "log_config": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "service_name", + "type": "string", + "required": true + } + ], + "returns": "void" + }, + "max_request_size": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "rate_limit": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "request_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "should_allow_host": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "host", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "ssl_cert_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "ssl_enabled": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "ssl_key_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "ssl_verify_mode": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "use_hsts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "validate_ssl_config": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.ssl_validation_result.SslValidationResult" + } + } + } + } + }, + "signalwire.core.skill_base": { + "classes": { + "SkillBase": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "agent": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "cleanup": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "define_tool": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "description", + "type": "string", + "required": true + }, + { + "name": "parameters", + "type": "any", + "required": true + }, + { + "name": "handler", + "type": "callable,class:signalwire.core.function_result.FunctionResult>", + "required": true + }, + { + "name": "secure", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "class:signalwire.core.swaig_function.ToolDefinition" + }, + "get_datamap_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_param_or_env": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true + }, + { + "name": "key", + "type": "string", + "required": true + }, + { + "name": "env_var", + "type": "string", + "required": true + }, + { + "name": "default_val", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_prompt_sections": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_skill_data": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "raw_data", + "type": "any", + "required": true + } + ], + "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "required_env_vars": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "required_packages": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "setup": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": true + } + ], + "returns": "bool" + }, + "skill_description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "skill_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "skill_version": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "supports_multiple_instances": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "update_skill_data": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "result", + "type": "class:signalwire.core.function_result.FunctionResult", + "required": true + }, + { + "name": "data", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "validate_env_vars": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "validate_packages": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.core.skill_manager": { + "classes": { + "SkillManager": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "agent": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.agent_base.AgentBase" + }, + "cleanup_all": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "get_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.skill_base.SkillBase" + }, + "has_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "is_loaded": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "list_loaded": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "list_loaded_skills": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "load_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true + }, + { + "name": "skill_class", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "bool" + }, + "unload_skill": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "skill_name", + "type": "string", + "required": true + } + ], + "returns": "void" + } + } + } + } + }, + "signalwire.core.swaig_actions_generated": { + "classes": { + "ContextSwitchAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "consolidate": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "full_reset": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "system_pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "system_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "user_pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "user_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "HoldAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "PlaybackBgAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "SwaigAction": { + "methods": { + "SWML": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "add_dynamic_hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "back_to_back_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "change_context": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "change_step": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "clear_dynamic_hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "context_switch": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "end_of_speech_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "extensive_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "functions_on_speaker_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hangup": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hold": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "playback_bg": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "replace_in_history": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "say": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "set_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "set_meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "settings": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "speech_event_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "stop": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "stop_playback_bg": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "toggle_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "unset_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "unset_meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "user_event": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "user_input": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait_for_user": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "SwaigResponse": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "action": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "post_process": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "response": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "TransferAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "dest": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "summarize": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.core.swaig_function": { + "classes": { + "SWAIGFunction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "handler", + "type": "class:signalwire.swaig_function_handler.SwaigFunctionHandler", + "required": true + }, + { + "name": "description", + "type": "string", + "required": true + }, + { + "name": "parameters", + "type": "any", + "required": false, + "default": null + }, + { + "name": "secure", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "fillers", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "wait_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "wait_file_loops", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "webhook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "required", + "type": "list", + "required": false, + "default": [] + }, + { + "name": "is_typed_handler", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "extra_swaig_fields", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "void" + }, + "call": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "execute": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "extra_swaig_fields": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "handler": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.swaig_function_handler.SwaigFunctionHandler" + }, + "is_external": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "is_typed_handler": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "parameters": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "required": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "secure": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "to_swaig": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "base_url", + "type": "string", + "required": true + }, + { + "name": "token", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "call_id", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "include_auth", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "any" + }, + "validate_args": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "args", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.args_validation_result.ArgsValidationResult" + }, + "wait_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "wait_file_loops": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "webhook_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + } + } + }, + "ToolDefinition": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "to_swaig_json": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "web_hook_url", + "type": "string", + "required": false, + "default": "" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.core.swaig_request_generated": { + "classes": { + "SwaigArgument": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "parsed": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "raw": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "substituted": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "SwaigRequest": { + "methods": { + "SWMLCall": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "SWMLVars": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "ai_session_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "app_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "args": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "argument": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "argument_desc": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_log": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "caller_id_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "caller_id_num": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "channel_active": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "channel_offhook": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "channel_ready": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "content_disposition": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "content_type": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "conversation_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "error_reason": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "fatal_error": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "function": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "input": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data_token": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "project_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "raw_call_log": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "space_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "version": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.core.swml_builder": { + "classes": { + "SWMLBuilder": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true + } + ], + "returns": "void" + }, + "add_section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "section_name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "ai": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "prompt_text", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt_pom", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "post_prompt", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "swaig", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "answer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "max_duration", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "codecs", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "build": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hangup": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "reason", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "play": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "urls", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "volume", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "say_voice", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "say_language", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "say_gender", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "auto_answer", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "render": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "reset": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "say": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "text", + "type": "string", + "required": true + }, + { + "name": "voice", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "language", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "gender", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "volume", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_builder.SWMLBuilder" + }, + "service": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + } + } + } + } + }, + "signalwire.core.swml_handler": { + "classes": { + "AIVerbHandler": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "build_config": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "prompt_text", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt_pom", + "type": "optional>>", + "required": false, + "default": null + }, + { + "name": "contexts", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "post_prompt", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "swaig", + "type": "optional>", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get_verb_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "validate_config": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "config", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + } + } + }, + "SWMLVerbHandler": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "build_config": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "kwargs", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get_verb_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "validate_config": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "config", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.verb_validation_result.VerbValidationResult" + } + } + }, + "VerbHandlerRegistry": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "get_handler": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.swml_handler.SWMLVerbHandler" + }, + "get_verb_names": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "has_handler": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "register_handler": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "handler", + "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", + "required": true + } + ], + "returns": "void" + } + } + } + } + }, + "signalwire.core.swml_renderer": { + "classes": { + "SwmlRenderer": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "render_function_response_swml": { + "params": [ + { + "name": "response_text", + "type": "string", + "required": true + }, + { + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true + }, + { + "name": "actions", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "format", + "type": "string", + "required": false, + "default": "json" + } + ], + "returns": "string" + }, + "render_swml": { + "params": [ + { + "name": "prompt", + "type": "any", + "required": true + }, + { + "name": "service", + "type": "class:signalwire.core.swml_service.SWMLService", + "required": true + }, + { + "name": "post_prompt", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "swaig_functions", + "type": "optional>>", + "required": false, + "default": null + }, + { + "name": "startup_hook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "hangup_hook_url", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt_is_pom", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "params", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "add_answer", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "record_call", + "type": "bool", + "required": false, + "default": false + }, + { + "name": "record_format", + "type": "string", + "required": false, + "default": "mp4" + }, + { + "name": "record_stereo", + "type": "bool", + "required": false, + "default": true + }, + { + "name": "format", + "type": "string", + "required": false, + "default": "json" + }, + { + "name": "default_webhook_url", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "string" + } + } + } + } + }, + "signalwire.core.swml_service": { + "classes": { + "SWMLService": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": false, + "default": "service" + }, + { + "name": "route", + "type": "string", + "required": false, + "default": "/" + }, + { + "name": "host", + "type": "string", + "required": false, + "default": "0.0.0.0" + }, + { + "name": "port", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "basic_auth", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "schema_path", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "config_file", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "schema_validation", + "type": "bool", + "required": false, + "default": true + } + ], + "returns": "void" + }, + "add_section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "section_name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "add_verb": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "config", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "add_verb_to_section": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "section_name", + "type": "string", + "required": true + }, + { + "name": "verb_name", + "type": "string", + "required": true + }, + { + "name": "config", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "ai": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "amazon_bedrock": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "answer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "as_router": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.core.web.HostAppRouter" + }, + "auth_password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "auth_username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "build_tool_registry_json": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "cond": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "connect": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "define_tool": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "tool", + "type": "class:signalwire.core.swaig_function.ToolDefinition", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "denoise": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" }, "detect_machine": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "document": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.swml.document.Document" + }, + "domain": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "enter_queue": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "execute": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "extract_introspect_payload": { + "params": [ + { + "name": "stdout_capture", + "type": "string", + "required": true + } + ], + "returns": "string" + }, + "extract_sip_username": { + "params": [ + { + "name": "request_body", + "type": "any", + "required": true + } + ], + "returns": "string" + }, + "full_validation_enabled": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "generate_random_hex": { + "params": [ + { + "name": "bytes", + "type": "int", + "required": true + } + ], + "returns": "string" + }, + "get_all_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "dict" + }, + "get_basic_auth_credentials": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "tuple" + }, + "get_basic_auth_credentials_with_source": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "tuple" + }, + "get_document": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_function": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.swaig_function.ToolDefinition" + }, + "get_routing_callback_paths": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "goto": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "handle_request": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "method", + "type": "string", + "required": true + }, + { + "name": "url", + "type": "string", + "required": true + }, + { + "name": "headers", + "type": "dict", + "required": true + }, + { + "name": "body", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "tuple,string>" + }, + "hangup": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "has_function": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "has_tool": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "host": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "join_conference": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "join_room": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "label": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "list_tool_names": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "live_transcribe": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "live_translate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "manual_set_proxy_url": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "proxy_url", + "type": "string", + "required": true + } + ], + "returns": "void" + }, + "name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "on_function_call": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + }, + { + "name": "args", + "type": "any", + "required": true + }, + { + "name": "raw_data", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.function_result.FunctionResult" + }, + "on_request": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "optional" + }, + "on_swml_request": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_data", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "callback_path", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "request", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "optional" + }, + "pay": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "play": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "port": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, + "prompt": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "receive_fax": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "record": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "record_call": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "register_routing_callback": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "callback", + "type": "class:signalwire.routing_callback.RoutingCallback", + "required": true + }, + { + "name": "path", + "type": "string", + "required": false, + "default": "/sip" + } + ], + "returns": "void" + }, + "register_swaig_function": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "func_def", + "type": "any", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "register_verb_handler": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "handler", + "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", + "required": true + } + ], + "returns": "void" + }, + "remove_function": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "render_document": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "render_swml": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "request": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "reset_document": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "return": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "route": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "schema_utils": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "class:signalwire.utils.schema_utils.SchemaUtils" + }, + "send_digits": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "send_fax": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "send_sms": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "serve": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "set": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "set_auth": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "username", + "type": "string", + "required": true + }, + { + "name": "password", + "type": "string", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "sip_refer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "sleep": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "milliseconds", + "type": "int", + "required": true + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "ssl_cert_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "ssl_enabled": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, + "ssl_key_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, + "stop": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "stop_denoise": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "stop_record_call": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "stop_tap": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "switch": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "tap": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "timing_safe_compare": { + "params": [ + { + "name": "a", + "type": "string", + "required": true + }, + { + "name": "b", + "type": "string", + "required": true + } + ], + "returns": "bool" + }, + "transfer": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "unset": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "user_event": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "params", + "type": "any", + "required": false, + "default": null + } + ], + "returns": "class:signalwire.core.swml_service.SWMLService" + }, + "validate_basic_auth": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "username", + "type": "string", + "required": true + }, + { + "name": "password", + "type": "string", + "required": true + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.core.swml_verbs_generated": { + "classes": { + "AI": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "ai": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIObject": { + "methods": { + "SWAIG": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "languages": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "post_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "post_prompt_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pronounce": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIParams": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "acknowledge_interruptions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ai_model": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ai_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ai_volume": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "app_name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "asr_diarize": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "asr_smart_format": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "asr_speaker_affinity": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "attention_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "attention_timeout_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "background_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "background_file_loops": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "background_file_volume": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "barge_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "barge_match_string": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "barge_min_words": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "conscience": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "conversation_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "conversation_sliding_window": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "convo": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "debug": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "debug_webhook_level": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "debug_webhook_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "digit_terminators": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "digit_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "direction": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "eleven_labs_similarity": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "eleven_labs_stability": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_barge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_inner_dialog": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_pause": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_thinking": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_turn_detection": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "enable_vision": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "end_of_speech_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "energy_level": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "first_word_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "function_wait_for_talking": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "functions_on_no_response": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hard_stop_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hard_stop_time": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hold_music": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hold_on_process": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "inactivity_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "initial_sleep_ms": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "inner_dialog_model": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "inner_dialog_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "inner_dialog_synced": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "input_poll_freq": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "interrupt_on_noise": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "interrupt_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "languages_enabled": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "llm_diarize_aware": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "local_tz": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_emotion": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_response_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "openai_asr_engine": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "outbound_attention_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "persist_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pom_format": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "save_conversation": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "speak_when_spoken_to": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "speech_event_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "speech_gen_quick_stops": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "speech_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "start_paused": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "static_greeting": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "static_greeting_no_barge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "summary_mode": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swaig_allow_settings": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swaig_allow_swml": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swaig_post_conversation": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swaig_post_swml_vars": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "swaig_set_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "thinking_model": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_summary": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transparent_barge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transparent_barge_max_time": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "tts_number_format": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "turn_detection_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "vad_config": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_idle_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_listening_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_talking_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "vision_model": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait_for_user": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wake_prefix": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIPostPromptPom": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "confidence": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "frequency_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "presence_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "temperature": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "top_p": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIPostPromptText": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "confidence": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "frequency_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "presence_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "temperature": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "text": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "top_p": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIPromptPom": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "confidence": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "contexts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "frequency_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "presence_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "temperature": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "top_p": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AIPromptText": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "confidence": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "contexts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "frequency_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_tokens": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "presence_penalty": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "temperature": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "text": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "top_p": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AiSidecar": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "ai_sidecar": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AiSidecarConfig": { + "methods": { + "SWAIG": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "action": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "customer_role": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "direction": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "lang": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "model": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "permissions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AllOfProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "allOf": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AmazonBedrock": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "amazon_bedrock": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AmazonBedrockObject": { + "methods": { + "SWAIG": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "post_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "post_prompt_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Answer": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "AnyOfProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "anyOf": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ArrayProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "default": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "items": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "nullable": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "type": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "BedrockParams": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "attention_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hard_stop_prompt": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "hard_stop_time": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "inactivity_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_idle_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_listening_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "video_talking_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "BedrockSWAIG": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "defaults": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "includes": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "native_functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "BooleanProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "default": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "nullable": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "type": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ChangeContextAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "change_context": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ChangeStepAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "change_step": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Cond": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "cond": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "CondReg": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "else": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "then": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "when": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Connect": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "connect": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer_on_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_events": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "codecs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "encryption": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_duration": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "parallel": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "result": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ringback": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "serial": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "serial_parallel": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "session_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "to": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectDeviceParallel": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer_on_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_events": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "codecs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "encryption": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_duration": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "parallel": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "result": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ringback": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "session_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectDeviceSerial": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer_on_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_events": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "codecs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "encryption": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_duration": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "result": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ringback": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "serial": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "session_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectDeviceSerialParallel": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer_on_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_events": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "codecs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "encryption": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_duration": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "result": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ringback": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "serial_parallel": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "session_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectDeviceSingle": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "answer_on_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_events": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "call_state_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "codecs": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "confirm_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "encryption": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "from": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "max_duration": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "password": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "result": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "ringback": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "session_timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "timeout": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "to": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "transfer_after_bridge": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "username": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webrtc_media": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectHeaders": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "value": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConnectSwitch": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "case": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "default": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "variable": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConstProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "const": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ContextPOMSteps": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "end": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "skip_user_turn": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "step_criteria": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "valid_contexts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "valid_steps": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ContextSwitchAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "context_switch": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ContextTextSteps": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "end": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "functions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "name": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "skip_user_turn": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "step_criteria": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "text": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "valid_contexts": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "valid_steps": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Contexts": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "default": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ContextsPOMObject": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "enter_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "exit_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "isolated": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "pom": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "steps": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ContextsTextObject": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "enter_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "exit_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "isolated": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "steps": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "text": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "ConversationMessage": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "content": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "lang": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "role": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "DataMap": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "expressions": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "output": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "webhooks": { + "params": [ + { + "name": "self", + "kind": "self" } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "document": { + "returns": "any" + } + } + }, + "Denoise": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.swml.document.Document" + "returns": "void" }, - "enter_queue": { + "denoise": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "execute": { + "returns": "any" + } + } + }, + "DetectMachine": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "extract_introspect_payload": { + "detect_machine": { "params": [ { - "name": "stdout_capture", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "string" - }, - "extract_sip_username": { + "returns": "any" + } + } + }, + "DetectMachineConfig": { + "methods": { + "__init__": { "params": [ { - "name": "request_body", - "type": "any", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "full_validation_enabled": { + "detect_message_end": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "get_all_functions": { + "detectors": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "dict" + "returns": "any" }, - "get_basic_auth_credentials": { + "end_silence_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "tuple" + "returns": "any" }, - "get_basic_auth_credentials_with_source": { + "initial_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "tuple" + "returns": "any" }, - "get_document": { + "machine_ready_timeout": { "params": [ { "name": "self", @@ -8960,470 +15588,314 @@ ], "returns": "any" }, - "get_function": { + "machine_voice_threshold": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.swaig_function.ToolDefinition" + "returns": "any" }, - "get_routing_callback_paths": { + "machine_words_threshold": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "goto": { + "status_url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "handle_request": { + "timeout": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "method", - "type": "string", - "required": true - }, - { - "name": "url", - "type": "string", - "required": true - }, - { - "name": "headers", - "type": "dict", - "required": true - }, - { - "name": "body", - "type": "optional", - "required": false, - "default": null } ], - "returns": "tuple,string>" + "returns": "any" }, - "hangup": { + "tone": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "has_function": { + "wait": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "bool" - }, - "has_tool": { + "returns": "any" + } + } + }, + "EnterQueue": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "void" }, - "host": { + "enter_queue": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "join_conference": { + "returns": "any" + } + } + }, + "EnterQueueObject": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "join_room": { + "queue_name": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "label": { + "status_url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "list_tool_names": { + "transfer_after_bridge": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "list" + "returns": "any" }, - "live_transcribe": { + "wait_time": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "live_translate": { + "wait_url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "manual_set_proxy_url": { + "returns": "any" + } + } + }, + "Execute": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "proxy_url", - "type": "string", - "required": true } ], "returns": "void" }, - "name": { + "execute": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "on_function_call": { + "returns": "any" + } + } + }, + "ExecuteConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true - }, - { - "name": "args", - "type": "any", - "required": true - }, - { - "name": "raw_data", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.function_result.FunctionResult" + "returns": "void" }, - "on_request": { + "dest": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_data", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "callback_path", - "type": "optional", - "required": false, - "default": null } ], - "returns": "optional" + "returns": "any" }, - "on_swml_request": { + "meta": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_data", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "callback_path", - "type": "optional", - "required": false, - "default": null } ], - "returns": "optional" + "returns": "any" }, - "pay": { + "on_return": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "play": { + "params": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "port": { + "result": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "int" - }, - "prompt": { + "returns": "any" + } + } + }, + "ExecuteSwitch": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "receive_fax": { + "case": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "record": { + "default": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "record_call": { + "variable": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "register_routing_callback": { + "returns": "any" + } + } + }, + "Expression": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "callback", - "type": "class:signalwire.routing_callback.RoutingCallback", - "required": true - }, - { - "name": "path", - "type": "string", - "required": false, - "default": null } ], "returns": "void" }, - "register_swaig_function": { + "output": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "func_def", - "type": "any", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "register_verb_handler": { + "pattern": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "handler", - "type": "class:signalwire.core.swml_handler.SWMLVerbHandler", - "required": true } ], - "returns": "void" + "returns": "any" }, - "remove_function": { + "string": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "name", - "type": "string", - "required": true } ], - "returns": "bool" - }, - "render_document": { + "returns": "any" + } + } + }, + "FunctionParameters": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" + "returns": "void" }, - "render_swml": { + "properties": { "params": [ { "name": "self", @@ -9432,109 +15904,91 @@ ], "returns": "any" }, - "request": { + "required": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "reset_document": { + "type": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" - }, - "return": { + "returns": "any" + } + } + }, + "Goto": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "void" }, - "route": { + "goto": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "string" - }, - "schema_utils": { + "returns": "any" + } + } + }, + "GotoConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "class:signalwire.utils.schema_utils.SchemaUtils" + "returns": "void" }, - "send_digits": { + "label": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "send_fax": { + "max": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "send_sms": { + "when": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" - }, - "serve": { + "returns": "any" + } + } + }, + "HangUpHookSWAIGFunction": { + "methods": { + "__init__": { "params": [ { "name": "self", @@ -9543,239 +15997,144 @@ ], "returns": "void" }, - "set": { + "active": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "set_auth": { + "argument": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "username", - "type": "string", - "required": true - }, - { - "name": "password", - "type": "string", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "sip_refer": { + "data_map": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "sleep": { + "description": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "milliseconds", - "type": "int", - "required": true } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "stop": { + "fillers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "stop_denoise": { + "function": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "stop_record_call": { + "meta_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "stop_tap": { + "meta_data_token": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "switch": { + "parameters": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "tap": { + "purpose": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "timing_safe_compare": { + "skip_fillers": { "params": [ { - "name": "a", - "type": "string", - "required": true - }, - { - "name": "b", - "type": "string", - "required": true + "name": "self", + "kind": "self" } ], - "returns": "bool" + "returns": "any" }, - "transfer": { + "wait_file": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "unset": { + "wait_file_loops": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "user_event": { + "wait_for_fillers": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "params", - "type": "any", - "required": false, - "default": null } ], - "returns": "class:signalwire.core.swml_service.SWMLService" + "returns": "any" }, - "validate_basic_auth": { + "web_hook_url": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "username", - "type": "string", - "required": true - }, - { - "name": "password", - "type": "string", - "required": true } ], - "returns": "bool" + "returns": "any" } } - } - } - }, - "signalwire.core.swml_verbs_generated": { - "classes": { - "AI": { + }, + "Hangup": { "methods": { "__init__": { "params": [ @@ -9786,7 +16145,7 @@ ], "returns": "void" }, - "ai": { + "hangup": { "params": [ { "name": "self", @@ -9797,17 +16156,30 @@ } } }, - "AIObject": { + "HangupAction": { "methods": { - "SWAIG": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, + "hangup": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Hint": { + "methods": { "__init__": { "params": [ { @@ -9817,7 +16189,7 @@ ], "returns": "void" }, - "hints": { + "hint": { "params": [ { "name": "self", @@ -9826,7 +16198,7 @@ ], "returns": "any" }, - "languages": { + "ignore_case": { "params": [ { "name": "self", @@ -9835,7 +16207,7 @@ ], "returns": "any" }, - "params": { + "pattern": { "params": [ { "name": "self", @@ -9844,7 +16216,7 @@ ], "returns": "any" }, - "post_prompt": { + "replace": { "params": [ { "name": "self", @@ -9852,17 +16224,21 @@ } ], "returns": "any" - }, - "prompt": { + } + } + }, + "HoldAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "pronounce": { + "hold": { "params": [ { "name": "self", @@ -9873,7 +16249,7 @@ } } }, - "AIParams": { + "InjectAction": { "methods": { "__init__": { "params": [ @@ -9884,7 +16260,7 @@ ], "returns": "void" }, - "acknowledge_interruptions": { + "inject": { "params": [ { "name": "self", @@ -9892,17 +16268,21 @@ } ], "returns": "any" - }, - "ai_volume": { + } + } + }, + "IntegerProperty": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "asr_diarize": { + "default": { "params": [ { "name": "self", @@ -9911,7 +16291,7 @@ ], "returns": "any" }, - "asr_smart_format": { + "description": { "params": [ { "name": "self", @@ -9920,7 +16300,7 @@ ], "returns": "any" }, - "asr_speaker_affinity": { + "enum": { "params": [ { "name": "self", @@ -9929,7 +16309,7 @@ ], "returns": "any" }, - "attention_timeout": { + "nullable": { "params": [ { "name": "self", @@ -9938,7 +16318,7 @@ ], "returns": "any" }, - "background_file_loops": { + "type": { "params": [ { "name": "self", @@ -9946,17 +16326,21 @@ } ], "returns": "any" - }, - "background_file_volume": { + } + } + }, + "JoinConference": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "barge_functions": { + "join_conference": { "params": [ { "name": "self", @@ -9964,17 +16348,21 @@ } ], "returns": "any" - }, - "barge_min_words": { + } + } + }, + "JoinConferenceObject": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "conversation_sliding_window": { + "beep": { "params": [ { "name": "self", @@ -9983,7 +16371,7 @@ ], "returns": "any" }, - "convo": { + "coach": { "params": [ { "name": "self", @@ -9992,7 +16380,7 @@ ], "returns": "any" }, - "debug": { + "end_on_exit": { "params": [ { "name": "self", @@ -10001,7 +16389,7 @@ ], "returns": "any" }, - "debug_webhook_level": { + "max_participants": { "params": [ { "name": "self", @@ -10010,7 +16398,7 @@ ], "returns": "any" }, - "digit_timeout": { + "muted": { "params": [ { "name": "self", @@ -10019,7 +16407,7 @@ ], "returns": "any" }, - "direction": { + "name": { "params": [ { "name": "self", @@ -10028,7 +16416,7 @@ ], "returns": "any" }, - "eleven_labs_similarity": { + "record": { "params": [ { "name": "self", @@ -10037,7 +16425,7 @@ ], "returns": "any" }, - "eleven_labs_stability": { + "recording_status_callback": { "params": [ { "name": "self", @@ -10046,7 +16434,7 @@ ], "returns": "any" }, - "enable_barge": { + "recording_status_callback_event": { "params": [ { "name": "self", @@ -10055,7 +16443,7 @@ ], "returns": "any" }, - "enable_inner_dialog": { + "recording_status_callback_method": { "params": [ { "name": "self", @@ -10064,7 +16452,7 @@ ], "returns": "any" }, - "enable_pause": { + "region": { "params": [ { "name": "self", @@ -10073,7 +16461,7 @@ ], "returns": "any" }, - "enable_thinking": { + "result": { "params": [ { "name": "self", @@ -10082,7 +16470,7 @@ ], "returns": "any" }, - "enable_turn_detection": { + "start_on_enter": { "params": [ { "name": "self", @@ -10091,7 +16479,7 @@ ], "returns": "any" }, - "enable_vision": { + "status_callback": { "params": [ { "name": "self", @@ -10100,7 +16488,7 @@ ], "returns": "any" }, - "end_of_speech_timeout": { + "status_callback_event": { "params": [ { "name": "self", @@ -10109,7 +16497,7 @@ ], "returns": "any" }, - "energy_level": { + "status_callback_method": { "params": [ { "name": "self", @@ -10118,7 +16506,7 @@ ], "returns": "any" }, - "first_word_timeout": { + "trim": { "params": [ { "name": "self", @@ -10127,7 +16515,7 @@ ], "returns": "any" }, - "function_wait_for_talking": { + "wait_url": { "params": [ { "name": "self", @@ -10135,17 +16523,21 @@ } ], "returns": "any" - }, - "functions_on_no_response": { + } + } + }, + "JoinRoom": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "hard_stop_time": { + "join_room": { "params": [ { "name": "self", @@ -10153,17 +16545,21 @@ } ], "returns": "any" - }, - "hold_on_process": { + } + } + }, + "JoinRoomConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "inactivity_timeout": { + "name": { "params": [ { "name": "self", @@ -10171,17 +16567,21 @@ } ], "returns": "any" - }, - "initial_sleep_ms": { + } + } + }, + "Label": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "inner_dialog_synced": { + "label": { "params": [ { "name": "self", @@ -10189,17 +16589,21 @@ } ], "returns": "any" - }, - "input_poll_freq": { + } + } + }, + "LanguageParams": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "interrupt_on_noise": { + "similarity": { "params": [ { "name": "self", @@ -10208,7 +16612,7 @@ ], "returns": "any" }, - "languages_enabled": { + "stability": { "params": [ { "name": "self", @@ -10216,17 +16620,21 @@ } ], "returns": "any" - }, - "llm_diarize_aware": { + } + } + }, + "LanguagesWithFillers": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "max_emotion": { + "code": { "params": [ { "name": "self", @@ -10235,7 +16643,7 @@ ], "returns": "any" }, - "max_response_tokens": { + "emotion": { "params": [ { "name": "self", @@ -10244,7 +16652,7 @@ ], "returns": "any" }, - "outbound_attention_timeout": { + "engine": { "params": [ { "name": "self", @@ -10253,7 +16661,7 @@ ], "returns": "any" }, - "persist_global_data": { + "function_fillers": { "params": [ { "name": "self", @@ -10262,7 +16670,7 @@ ], "returns": "any" }, - "save_conversation": { + "model": { "params": [ { "name": "self", @@ -10271,7 +16679,7 @@ ], "returns": "any" }, - "speak_when_spoken_to": { + "name": { "params": [ { "name": "self", @@ -10280,7 +16688,7 @@ ], "returns": "any" }, - "speech_event_timeout": { + "params": { "params": [ { "name": "self", @@ -10289,7 +16697,7 @@ ], "returns": "any" }, - "speech_gen_quick_stops": { + "speech_fillers": { "params": [ { "name": "self", @@ -10298,7 +16706,7 @@ ], "returns": "any" }, - "speech_timeout": { + "speed": { "params": [ { "name": "self", @@ -10307,7 +16715,7 @@ ], "returns": "any" }, - "start_paused": { + "voice": { "params": [ { "name": "self", @@ -10315,17 +16723,21 @@ } ], "returns": "any" - }, - "static_greeting_no_barge": { + } + } + }, + "LanguagesWithSoloFillers": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "summary_mode": { + "code": { "params": [ { "name": "self", @@ -10334,7 +16746,7 @@ ], "returns": "any" }, - "swaig_allow_settings": { + "emotion": { "params": [ { "name": "self", @@ -10343,7 +16755,7 @@ ], "returns": "any" }, - "swaig_allow_swml": { + "engine": { "params": [ { "name": "self", @@ -10352,7 +16764,7 @@ ], "returns": "any" }, - "swaig_post_conversation": { + "fillers": { "params": [ { "name": "self", @@ -10361,7 +16773,7 @@ ], "returns": "any" }, - "swaig_post_swml_vars": { + "model": { "params": [ { "name": "self", @@ -10370,7 +16782,7 @@ ], "returns": "any" }, - "swaig_set_global_data": { + "name": { "params": [ { "name": "self", @@ -10379,7 +16791,7 @@ ], "returns": "any" }, - "transfer_summary": { + "params": { "params": [ { "name": "self", @@ -10388,7 +16800,7 @@ ], "returns": "any" }, - "transparent_barge": { + "speed": { "params": [ { "name": "self", @@ -10397,7 +16809,7 @@ ], "returns": "any" }, - "transparent_barge_max_time": { + "voice": { "params": [ { "name": "self", @@ -10405,17 +16817,21 @@ } ], "returns": "any" - }, - "turn_detection_timeout": { + } + } + }, + "LiveTranscribe": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "wait_for_user": { + "live_transcribe": { "params": [ { "name": "self", @@ -10426,7 +16842,7 @@ } } }, - "AIPostPromptPom": { + "LiveTranscribeConfig": { "methods": { "__init__": { "params": [ @@ -10437,7 +16853,7 @@ ], "returns": "void" }, - "confidence": { + "action": { "params": [ { "name": "self", @@ -10445,17 +16861,21 @@ } ], "returns": "any" - }, - "frequency_penalty": { + } + } + }, + "LiveTranslate": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "pom": { + "live_translate": { "params": [ { "name": "self", @@ -10463,8 +16883,21 @@ } ], "returns": "any" + } + } + }, + "LiveTranslateConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "presence_penalty": { + "action": { "params": [ { "name": "self", @@ -10472,8 +16905,21 @@ } ], "returns": "any" + } + } + }, + "NullProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "temperature": { + "description": { "params": [ { "name": "self", @@ -10482,7 +16928,7 @@ ], "returns": "any" }, - "top_p": { + "type": { "params": [ { "name": "self", @@ -10493,7 +16939,7 @@ } } }, - "AIPostPromptText": { + "NumberProperty": { "methods": { "__init__": { "params": [ @@ -10504,7 +16950,7 @@ ], "returns": "void" }, - "confidence": { + "default": { "params": [ { "name": "self", @@ -10513,7 +16959,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "description": { "params": [ { "name": "self", @@ -10522,7 +16968,7 @@ ], "returns": "any" }, - "presence_penalty": { + "enum": { "params": [ { "name": "self", @@ -10531,7 +16977,7 @@ ], "returns": "any" }, - "temperature": { + "nullable": { "params": [ { "name": "self", @@ -10540,7 +16986,7 @@ ], "returns": "any" }, - "top_p": { + "type": { "params": [ { "name": "self", @@ -10551,7 +16997,7 @@ } } }, - "AIPromptPom": { + "ObjectProperty": { "methods": { "__init__": { "params": [ @@ -10562,16 +17008,7 @@ ], "returns": "void" }, - "confidence": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "contexts": { + "default": { "params": [ { "name": "self", @@ -10580,7 +17017,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "description": { "params": [ { "name": "self", @@ -10589,7 +17026,7 @@ ], "returns": "any" }, - "pom": { + "nullable": { "params": [ { "name": "self", @@ -10598,7 +17035,7 @@ ], "returns": "any" }, - "presence_penalty": { + "properties": { "params": [ { "name": "self", @@ -10607,7 +17044,7 @@ ], "returns": "any" }, - "temperature": { + "required": { "params": [ { "name": "self", @@ -10616,7 +17053,7 @@ ], "returns": "any" }, - "top_p": { + "type": { "params": [ { "name": "self", @@ -10627,7 +17064,7 @@ } } }, - "AIPromptText": { + "OmitPropertiesBedrockPostPomptTextOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -10647,7 +17084,7 @@ ], "returns": "any" }, - "contexts": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10656,7 +17093,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "max_tokens": { "params": [ { "name": "self", @@ -10683,20 +17120,7 @@ ], "returns": "any" }, - "top_p": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - } - } - }, - "AiSidecarConfig": { - "methods": { - "SWAIG": { + "text": { "params": [ { "name": "self", @@ -10705,18 +17129,18 @@ ], "returns": "any" }, - "__init__": { + "top_p": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" } } }, - "AllOfProperty": { + "OmitPropertiesBedrockPostPromptPomOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -10727,7 +17151,7 @@ ], "returns": "void" }, - "allOf": { + "confidence": { "params": [ { "name": "self", @@ -10735,21 +17159,8 @@ } ], "returns": "any" - } - } - }, - "AmazonBedrock": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "amazon_bedrock": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10757,12 +17168,8 @@ } ], "returns": "any" - } - } - }, - "AmazonBedrockObject": { - "methods": { - "SWAIG": { + }, + "max_tokens": { "params": [ { "name": "self", @@ -10771,16 +17178,16 @@ ], "returns": "any" }, - "__init__": { + "pom": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "params": { + "presence_penalty": { "params": [ { "name": "self", @@ -10789,7 +17196,7 @@ ], "returns": "any" }, - "post_prompt": { + "temperature": { "params": [ { "name": "self", @@ -10798,7 +17205,7 @@ ], "returns": "any" }, - "prompt": { + "top_p": { "params": [ { "name": "self", @@ -10809,7 +17216,7 @@ } } }, - "AnyOfProperty": { + "OmitPropertiesBedrockPromptPomOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -10820,29 +17227,25 @@ ], "returns": "void" }, - "anyOf": { + "confidence": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" - } - } - }, - "ArrayProperty": { - "methods": { - "__init__": { + "returns": "any" + }, + "frequency_penalty": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "items": { + "max_tokens": { "params": [ { "name": "self", @@ -10851,7 +17254,7 @@ ], "returns": "any" }, - "nullable": { + "pom": { "params": [ { "name": "self", @@ -10859,21 +17262,17 @@ } ], "returns": "any" - } - } - }, - "BedrockParams": { - "methods": { - "__init__": { + }, + "presence_penalty": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "attention_timeout": { + "temperature": { "params": [ { "name": "self", @@ -10882,7 +17281,7 @@ ], "returns": "any" }, - "hard_stop_time": { + "top_p": { "params": [ { "name": "self", @@ -10891,7 +17290,7 @@ ], "returns": "any" }, - "inactivity_timeout": { + "voice_id": { "params": [ { "name": "self", @@ -10902,7 +17301,7 @@ } } }, - "BedrockSWAIG": { + "OmitPropertiesBedrockPromptTextOmittedPromptProps": { "methods": { "__init__": { "params": [ @@ -10913,7 +17312,7 @@ ], "returns": "void" }, - "defaults": { + "confidence": { "params": [ { "name": "self", @@ -10922,7 +17321,7 @@ ], "returns": "any" }, - "functions": { + "frequency_penalty": { "params": [ { "name": "self", @@ -10931,7 +17330,7 @@ ], "returns": "any" }, - "includes": { + "max_tokens": { "params": [ { "name": "self", @@ -10940,7 +17339,7 @@ ], "returns": "any" }, - "native_functions": { + "presence_penalty": { "params": [ { "name": "self", @@ -10948,21 +17347,8 @@ } ], "returns": "any" - } - } - }, - "BooleanProperty": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "default": { + "temperature": { "params": [ { "name": "self", @@ -10971,7 +17357,7 @@ ], "returns": "any" }, - "nullable": { + "text": { "params": [ { "name": "self", @@ -10979,21 +17365,17 @@ } ], "returns": "any" - } - } - }, - "Cond": { - "methods": { - "__init__": { + }, + "top_p": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "cond": { + "voice_id": { "params": [ { "name": "self", @@ -11004,7 +17386,7 @@ } } }, - "CondReg": { + "OneOfProperty": { "methods": { "__init__": { "params": [ @@ -11015,7 +17397,7 @@ ], "returns": "void" }, - "then": { + "oneOf": { "params": [ { "name": "self", @@ -11026,7 +17408,7 @@ } } }, - "Connect": { + "Output": { "methods": { "__init__": { "params": [ @@ -11037,7 +17419,16 @@ ], "returns": "void" }, - "connect": { + "action": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "response": { "params": [ { "name": "self", @@ -11048,7 +17439,7 @@ } } }, - "ConnectConfig": { + "Pay": { "methods": { "__init__": { "params": [ @@ -11059,7 +17450,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "pay": { "params": [ { "name": "self", @@ -11067,17 +17458,21 @@ } ], "returns": "any" - }, - "call_state_events": { + } + } + }, + "PayConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "confirm": { + "charge_amount": { "params": [ { "name": "self", @@ -11086,7 +17481,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "currency": { "params": [ { "name": "self", @@ -11095,7 +17490,7 @@ ], "returns": "any" }, - "headers": { + "description": { "params": [ { "name": "self", @@ -11104,7 +17499,7 @@ ], "returns": "any" }, - "max_duration": { + "input": { "params": [ { "name": "self", @@ -11113,7 +17508,7 @@ ], "returns": "any" }, - "parallel": { + "language": { "params": [ { "name": "self", @@ -11122,7 +17517,7 @@ ], "returns": "any" }, - "result": { + "max_attempts": { "params": [ { "name": "self", @@ -11131,7 +17526,7 @@ ], "returns": "any" }, - "ringback": { + "min_postal_code_length": { "params": [ { "name": "self", @@ -11140,7 +17535,7 @@ ], "returns": "any" }, - "serial": { + "parameters": { "params": [ { "name": "self", @@ -11149,7 +17544,7 @@ ], "returns": "any" }, - "serial_parallel": { + "payment_connector_url": { "params": [ { "name": "self", @@ -11158,7 +17553,7 @@ ], "returns": "any" }, - "session_timeout": { + "payment_method": { "params": [ { "name": "self", @@ -11167,7 +17562,7 @@ ], "returns": "any" }, - "timeout": { + "postal_code": { "params": [ { "name": "self", @@ -11176,7 +17571,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "prompts": { "params": [ { "name": "self", @@ -11185,7 +17580,7 @@ ], "returns": "any" }, - "webrtc_media": { + "security_code": { "params": [ { "name": "self", @@ -11193,21 +17588,17 @@ } ], "returns": "any" - } - } - }, - "ConnectDeviceParallel": { - "methods": { - "__init__": { + }, + "status_url": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "answer_on_bridge": { + "timeout": { "params": [ { "name": "self", @@ -11216,7 +17607,7 @@ ], "returns": "any" }, - "call_state_events": { + "token_type": { "params": [ { "name": "self", @@ -11225,7 +17616,7 @@ ], "returns": "any" }, - "confirm": { + "valid_card_types": { "params": [ { "name": "self", @@ -11234,7 +17625,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "voice": { "params": [ { "name": "self", @@ -11242,17 +17633,21 @@ } ], "returns": "any" - }, - "headers": { + } + } + }, + "PayParameters": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "max_duration": { + "name": { "params": [ { "name": "self", @@ -11261,7 +17656,7 @@ ], "returns": "any" }, - "parallel": { + "value": { "params": [ { "name": "self", @@ -11269,17 +17664,21 @@ } ], "returns": "any" - }, - "result": { + } + } + }, + "PayPromptPlayAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "ringback": { + "phrase": { "params": [ { "name": "self", @@ -11288,7 +17687,7 @@ ], "returns": "any" }, - "session_timeout": { + "type": { "params": [ { "name": "self", @@ -11296,17 +17695,21 @@ } ], "returns": "any" - }, - "timeout": { + } + } + }, + "PayPromptSayAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "transfer_after_bridge": { + "phrase": { "params": [ { "name": "self", @@ -11315,7 +17718,7 @@ ], "returns": "any" }, - "webrtc_media": { + "type": { "params": [ { "name": "self", @@ -11326,7 +17729,7 @@ } } }, - "ConnectDeviceSerial": { + "PayPrompts": { "methods": { "__init__": { "params": [ @@ -11337,7 +17740,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "actions": { "params": [ { "name": "self", @@ -11346,7 +17749,7 @@ ], "returns": "any" }, - "call_state_events": { + "attempts": { "params": [ { "name": "self", @@ -11355,7 +17758,7 @@ ], "returns": "any" }, - "confirm": { + "card_type": { "params": [ { "name": "self", @@ -11364,7 +17767,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "error_type": { "params": [ { "name": "self", @@ -11373,7 +17776,7 @@ ], "returns": "any" }, - "headers": { + "for": { "params": [ { "name": "self", @@ -11381,8 +17784,21 @@ } ], "returns": "any" + } + } + }, + "PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "max_duration": { + "active": { "params": [ { "name": "self", @@ -11391,7 +17807,7 @@ ], "returns": "any" }, - "result": { + "data_map": { "params": [ { "name": "self", @@ -11400,7 +17816,7 @@ ], "returns": "any" }, - "ringback": { + "description": { "params": [ { "name": "self", @@ -11409,7 +17825,7 @@ ], "returns": "any" }, - "serial": { + "function": { "params": [ { "name": "self", @@ -11418,7 +17834,7 @@ ], "returns": "any" }, - "session_timeout": { + "meta_data": { "params": [ { "name": "self", @@ -11427,7 +17843,7 @@ ], "returns": "any" }, - "timeout": { + "meta_data_token": { "params": [ { "name": "self", @@ -11436,7 +17852,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "parameters": { "params": [ { "name": "self", @@ -11445,7 +17861,7 @@ ], "returns": "any" }, - "webrtc_media": { + "web_hook_url": { "params": [ { "name": "self", @@ -11456,7 +17872,7 @@ } } }, - "ConnectDeviceSerialParallel": { + "PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps": { "methods": { "__init__": { "params": [ @@ -11467,7 +17883,7 @@ ], "returns": "void" }, - "answer_on_bridge": { + "active": { "params": [ { "name": "self", @@ -11476,7 +17892,7 @@ ], "returns": "any" }, - "call_state_events": { + "data_map": { "params": [ { "name": "self", @@ -11485,7 +17901,7 @@ ], "returns": "any" }, - "confirm": { + "description": { "params": [ { "name": "self", @@ -11494,7 +17910,7 @@ ], "returns": "any" }, - "confirm_timeout": { + "function": { "params": [ { "name": "self", @@ -11503,7 +17919,7 @@ ], "returns": "any" }, - "headers": { + "meta_data": { "params": [ { "name": "self", @@ -11512,7 +17928,7 @@ ], "returns": "any" }, - "max_duration": { + "meta_data_token": { "params": [ { "name": "self", @@ -11521,7 +17937,7 @@ ], "returns": "any" }, - "result": { + "parameters": { "params": [ { "name": "self", @@ -11530,7 +17946,7 @@ ], "returns": "any" }, - "ringback": { + "web_hook_url": { "params": [ { "name": "self", @@ -11538,17 +17954,21 @@ } ], "returns": "any" - }, - "serial_parallel": { + } + } + }, + "PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "session_timeout": { + "active": { "params": [ { "name": "self", @@ -11557,7 +17977,7 @@ ], "returns": "any" }, - "timeout": { + "data_map": { "params": [ { "name": "self", @@ -11566,7 +17986,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "description": { "params": [ { "name": "self", @@ -11575,7 +17995,7 @@ ], "returns": "any" }, - "webrtc_media": { + "function": { "params": [ { "name": "self", @@ -11583,21 +18003,17 @@ } ], "returns": "any" - } - } - }, - "ConnectDeviceSingle": { - "methods": { - "__init__": { + }, + "meta_data": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "answer_on_bridge": { + "meta_data_token": { "params": [ { "name": "self", @@ -11606,7 +18022,7 @@ ], "returns": "any" }, - "call_state_events": { + "parameters": { "params": [ { "name": "self", @@ -11615,7 +18031,7 @@ ], "returns": "any" }, - "confirm": { + "web_hook_url": { "params": [ { "name": "self", @@ -11623,17 +18039,21 @@ } ], "returns": "any" - }, - "confirm_timeout": { + } + } + }, + "PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "headers": { + "active": { "params": [ { "name": "self", @@ -11642,7 +18062,7 @@ ], "returns": "any" }, - "max_duration": { + "data_map": { "params": [ { "name": "self", @@ -11651,7 +18071,7 @@ ], "returns": "any" }, - "result": { + "description": { "params": [ { "name": "self", @@ -11660,7 +18080,7 @@ ], "returns": "any" }, - "ringback": { + "function": { "params": [ { "name": "self", @@ -11669,7 +18089,7 @@ ], "returns": "any" }, - "session_timeout": { + "meta_data": { "params": [ { "name": "self", @@ -11678,7 +18098,7 @@ ], "returns": "any" }, - "timeout": { + "meta_data_token": { "params": [ { "name": "self", @@ -11687,7 +18107,7 @@ ], "returns": "any" }, - "transfer_after_bridge": { + "parameters": { "params": [ { "name": "self", @@ -11696,7 +18116,7 @@ ], "returns": "any" }, - "webrtc_media": { + "web_hook_url": { "params": [ { "name": "self", @@ -11707,7 +18127,7 @@ } } }, - "ConnectSwitch": { + "Play": { "methods": { "__init__": { "params": [ @@ -11718,7 +18138,7 @@ ], "returns": "void" }, - "default": { + "play": { "params": [ { "name": "self", @@ -11729,7 +18149,7 @@ } } }, - "ContextPOMSteps": { + "PlayWithURL": { "methods": { "__init__": { "params": [ @@ -11740,7 +18160,7 @@ ], "returns": "void" }, - "pom": { + "auto_answer": { "params": [ { "name": "self", @@ -11749,7 +18169,7 @@ ], "returns": "any" }, - "skip_user_turn": { + "say_gender": { "params": [ { "name": "self", @@ -11757,21 +18177,17 @@ } ], "returns": "any" - } - } - }, - "ContextTextSteps": { - "methods": { - "__init__": { + }, + "say_language": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "skip_user_turn": { + "say_voice": { "params": [ { "name": "self", @@ -11779,21 +18195,26 @@ } ], "returns": "any" - } - } - }, - "Contexts": { - "methods": { - "__init__": { + }, + "status_url": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "volume": { "params": [ { "name": "self", @@ -11804,7 +18225,7 @@ } } }, - "ContextsPOMObject": { + "PlayWithURLS": { "methods": { "__init__": { "params": [ @@ -11815,7 +18236,7 @@ ], "returns": "void" }, - "enter_fillers": { + "auto_answer": { "params": [ { "name": "self", @@ -11824,7 +18245,7 @@ ], "returns": "any" }, - "exit_fillers": { + "say_gender": { "params": [ { "name": "self", @@ -11833,7 +18254,7 @@ ], "returns": "any" }, - "pom": { + "say_language": { "params": [ { "name": "self", @@ -11842,7 +18263,7 @@ ], "returns": "any" }, - "steps": { + "say_voice": { "params": [ { "name": "self", @@ -11850,21 +18271,8 @@ } ], "returns": "any" - } - } - }, - "ContextsTextObject": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "enter_fillers": { + "status_url": { "params": [ { "name": "self", @@ -11873,7 +18281,7 @@ ], "returns": "any" }, - "exit_fillers": { + "urls": { "params": [ { "name": "self", @@ -11882,7 +18290,7 @@ ], "returns": "any" }, - "steps": { + "volume": { "params": [ { "name": "self", @@ -11893,7 +18301,7 @@ } } }, - "ConversationMessage": { + "PlaybackBGAction": { "methods": { "__init__": { "params": [ @@ -11904,7 +18312,7 @@ ], "returns": "void" }, - "role": { + "playback_bg": { "params": [ { "name": "self", @@ -11915,7 +18323,7 @@ } } }, - "DataMap": { + "PomSectionBodyContent": { "methods": { "__init__": { "params": [ @@ -11926,7 +18334,7 @@ ], "returns": "void" }, - "expressions": { + "body": { "params": [ { "name": "self", @@ -11935,7 +18343,7 @@ ], "returns": "any" }, - "output": { + "bullets": { "params": [ { "name": "self", @@ -11944,7 +18352,7 @@ ], "returns": "any" }, - "webhooks": { + "numbered": { "params": [ { "name": "self", @@ -11952,21 +18360,17 @@ } ], "returns": "any" - } - } - }, - "DetectMachineConfig": { - "methods": { - "__init__": { + }, + "numberedBullets": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "detect_message_end": { + "subsections": { "params": [ { "name": "self", @@ -11975,7 +18379,7 @@ ], "returns": "any" }, - "end_silence_timeout": { + "title": { "params": [ { "name": "self", @@ -11983,8 +18387,21 @@ } ], "returns": "any" + } + } + }, + "PomSectionBulletsContent": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "initial_timeout": { + "body": { "params": [ { "name": "self", @@ -11993,7 +18410,7 @@ ], "returns": "any" }, - "machine_ready_timeout": { + "bullets": { "params": [ { "name": "self", @@ -12002,7 +18419,7 @@ ], "returns": "any" }, - "machine_voice_threshold": { + "numbered": { "params": [ { "name": "self", @@ -12011,7 +18428,7 @@ ], "returns": "any" }, - "machine_words_threshold": { + "numberedBullets": { "params": [ { "name": "self", @@ -12020,7 +18437,7 @@ ], "returns": "any" }, - "timeout": { + "subsections": { "params": [ { "name": "self", @@ -12029,7 +18446,7 @@ ], "returns": "any" }, - "wait": { + "title": { "params": [ { "name": "self", @@ -12040,7 +18457,7 @@ } } }, - "EnterQueue": { + "Prompt": { "methods": { "__init__": { "params": [ @@ -12051,7 +18468,7 @@ ], "returns": "void" }, - "enter_queue": { + "prompt": { "params": [ { "name": "self", @@ -12062,7 +18479,7 @@ } } }, - "EnterQueueObject": { + "PromptConfig": { "methods": { "__init__": { "params": [ @@ -12073,7 +18490,7 @@ ], "returns": "void" }, - "transfer_after_bridge": { + "digit_timeout": { "params": [ { "name": "self", @@ -12082,7 +18499,7 @@ ], "returns": "any" }, - "wait_time": { + "initial_timeout": { "params": [ { "name": "self", @@ -12091,7 +18508,7 @@ ], "returns": "any" }, - "wait_url": { + "max_digits": { "params": [ { "name": "self", @@ -12099,21 +18516,8 @@ } ], "returns": "any" - } - } - }, - "ExecuteConfig": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "on_return": { + "play": { "params": [ { "name": "self", @@ -12122,7 +18526,7 @@ ], "returns": "any" }, - "result": { + "say_gender": { "params": [ { "name": "self", @@ -12130,21 +18534,17 @@ } ], "returns": "any" - } - } - }, - "ExecuteSwitch": { - "methods": { - "__init__": { + }, + "say_language": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "say_voice": { "params": [ { "name": "self", @@ -12152,21 +18552,17 @@ } ], "returns": "any" - } - } - }, - "Expression": { - "methods": { - "__init__": { + }, + "speech_end_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "output": { + "speech_engine": { "params": [ { "name": "self", @@ -12174,21 +18570,17 @@ } ], "returns": "any" - } - } - }, - "GotoConfig": { - "methods": { - "__init__": { + }, + "speech_hints": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "max": { + "speech_language": { "params": [ { "name": "self", @@ -12196,21 +18588,17 @@ } ], "returns": "any" - } - } - }, - "HangUpHookSWAIGFunction": { - "methods": { - "__init__": { + }, + "speech_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "active": { + "status_url": { "params": [ { "name": "self", @@ -12219,7 +18607,7 @@ ], "returns": "any" }, - "argument": { + "terminators": { "params": [ { "name": "self", @@ -12228,7 +18616,7 @@ ], "returns": "any" }, - "data_map": { + "volume": { "params": [ { "name": "self", @@ -12236,17 +18624,21 @@ } ], "returns": "any" - }, - "fillers": { + } + } + }, + "Pronounce": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "parameters": { + "ignore_case": { "params": [ { "name": "self", @@ -12255,7 +18647,7 @@ ], "returns": "any" }, - "skip_fillers": { + "replace": { "params": [ { "name": "self", @@ -12264,7 +18656,7 @@ ], "returns": "any" }, - "wait_for_fillers": { + "with": { "params": [ { "name": "self", @@ -12275,7 +18667,7 @@ } } }, - "HangupAction": { + "ReceiveFax": { "methods": { "__init__": { "params": [ @@ -12286,7 +18678,7 @@ ], "returns": "void" }, - "hangup": { + "receive_fax": { "params": [ { "name": "self", @@ -12297,7 +18689,7 @@ } } }, - "Hint": { + "ReceiveFaxConfig": { "methods": { "__init__": { "params": [ @@ -12308,7 +18700,7 @@ ], "returns": "void" }, - "ignore_case": { + "status_url": { "params": [ { "name": "self", @@ -12319,7 +18711,7 @@ } } }, - "HoldAction": { + "Record": { "methods": { "__init__": { "params": [ @@ -12330,7 +18722,7 @@ ], "returns": "void" }, - "hold": { + "record": { "params": [ { "name": "self", @@ -12341,7 +18733,7 @@ } } }, - "IntegerProperty": { + "RecordCall": { "methods": { "__init__": { "params": [ @@ -12352,16 +18744,7 @@ ], "returns": "void" }, - "default": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "any" - }, - "nullable": { + "record_call": { "params": [ { "name": "self", @@ -12372,7 +18755,7 @@ } } }, - "JoinConference": { + "RecordCallConfig": { "methods": { "__init__": { "params": [ @@ -12383,7 +18766,7 @@ ], "returns": "void" }, - "join_conference": { + "beep": { "params": [ { "name": "self", @@ -12391,21 +18774,17 @@ } ], "returns": "any" - } - } - }, - "JoinConferenceObject": { - "methods": { - "__init__": { + }, + "control_id": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "end_on_exit": { + "direction": { "params": [ { "name": "self", @@ -12414,7 +18793,7 @@ ], "returns": "any" }, - "max_participants": { + "end_silence_timeout": { "params": [ { "name": "self", @@ -12423,7 +18802,7 @@ ], "returns": "any" }, - "muted": { + "format": { "params": [ { "name": "self", @@ -12432,7 +18811,7 @@ ], "returns": "any" }, - "result": { + "initial_timeout": { "params": [ { "name": "self", @@ -12441,7 +18820,7 @@ ], "returns": "any" }, - "start_on_enter": { + "input_sensitivity": { "params": [ { "name": "self", @@ -12450,7 +18829,7 @@ ], "returns": "any" }, - "wait_url": { + "max_length": { "params": [ { "name": "self", @@ -12458,21 +18837,17 @@ } ], "returns": "any" - } - } - }, - "LanguageParams": { - "methods": { - "__init__": { + }, + "status_url": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "similarity": { + "stereo": { "params": [ { "name": "self", @@ -12481,7 +18856,7 @@ ], "returns": "any" }, - "stability": { + "terminators": { "params": [ { "name": "self", @@ -12492,7 +18867,7 @@ } } }, - "LanguagesWithFillers": { + "RecordConfig": { "methods": { "__init__": { "params": [ @@ -12503,7 +18878,7 @@ ], "returns": "void" }, - "params": { + "beep": { "params": [ { "name": "self", @@ -12511,21 +18886,8 @@ } ], "returns": "any" - } - } - }, - "LanguagesWithSoloFillers": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "void" }, - "params": { + "direction": { "params": [ { "name": "self", @@ -12533,21 +18895,17 @@ } ], "returns": "any" - } - } - }, - "LiveTranscribeConfig": { - "methods": { - "__init__": { + }, + "end_silence_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "action": { + "format": { "params": [ { "name": "self", @@ -12555,21 +18913,17 @@ } ], "returns": "any" - } - } - }, - "LiveTranslateConfig": { - "methods": { - "__init__": { + }, + "initial_timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "action": { + "input_sensitivity": { "params": [ { "name": "self", @@ -12577,21 +18931,17 @@ } ], "returns": "any" - } - } - }, - "NumberProperty": { - "methods": { - "__init__": { + }, + "max_length": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "default": { + "status_url": { "params": [ { "name": "self", @@ -12600,7 +18950,7 @@ ], "returns": "any" }, - "enum": { + "stereo": { "params": [ { "name": "self", @@ -12609,7 +18959,7 @@ ], "returns": "any" }, - "nullable": { + "terminators": { "params": [ { "name": "self", @@ -12620,7 +18970,7 @@ } } }, - "ObjectProperty": { + "Request": { "methods": { "__init__": { "params": [ @@ -12631,7 +18981,7 @@ ], "returns": "void" }, - "nullable": { + "request": { "params": [ { "name": "self", @@ -12642,7 +18992,7 @@ } } }, - "OmitPropertiesBedrockPostPomptTextOmittedPromptProps": { + "RequestConfig": { "methods": { "__init__": { "params": [ @@ -12653,7 +19003,7 @@ ], "returns": "void" }, - "confidence": { + "body": { "params": [ { "name": "self", @@ -12662,7 +19012,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "connect_timeout": { "params": [ { "name": "self", @@ -12671,7 +19021,7 @@ ], "returns": "any" }, - "presence_penalty": { + "headers": { "params": [ { "name": "self", @@ -12680,7 +19030,7 @@ ], "returns": "any" }, - "temperature": { + "method": { "params": [ { "name": "self", @@ -12689,7 +19039,7 @@ ], "returns": "any" }, - "top_p": { + "save_variables": { "params": [ { "name": "self", @@ -12697,21 +19047,17 @@ } ], "returns": "any" - } - } - }, - "OmitPropertiesBedrockPostPromptPomOmittedPromptProps": { - "methods": { - "__init__": { + }, + "timeout": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "confidence": { + "url": { "params": [ { "name": "self", @@ -12719,17 +19065,21 @@ } ], "returns": "any" - }, - "frequency_penalty": { + } + } + }, + "RingbackConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "pom": { + "auto_answer": { "params": [ { "name": "self", @@ -12738,7 +19088,7 @@ ], "returns": "any" }, - "presence_penalty": { + "loop": { "params": [ { "name": "self", @@ -12747,7 +19097,7 @@ ], "returns": "any" }, - "temperature": { + "say_gender": { "params": [ { "name": "self", @@ -12756,7 +19106,7 @@ ], "returns": "any" }, - "top_p": { + "say_language": { "params": [ { "name": "self", @@ -12764,21 +19114,17 @@ } ], "returns": "any" - } - } - }, - "OmitPropertiesBedrockPromptPomOmittedPromptProps": { - "methods": { - "__init__": { + }, + "say_voice": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "confidence": { + "status_url": { "params": [ { "name": "self", @@ -12787,7 +19133,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "url": { "params": [ { "name": "self", @@ -12796,7 +19142,7 @@ ], "returns": "any" }, - "pom": { + "urls": { "params": [ { "name": "self", @@ -12805,7 +19151,7 @@ ], "returns": "any" }, - "presence_penalty": { + "volume": { "params": [ { "name": "self", @@ -12813,17 +19159,21 @@ } ], "returns": "any" - }, - "temperature": { + } + } + }, + "SIPRefer": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "top_p": { + "sip_refer": { "params": [ { "name": "self", @@ -12834,7 +19184,7 @@ } } }, - "OmitPropertiesBedrockPromptTextOmittedPromptProps": { + "SMSWithBody": { "methods": { "__init__": { "params": [ @@ -12845,7 +19195,7 @@ ], "returns": "void" }, - "confidence": { + "body": { "params": [ { "name": "self", @@ -12854,7 +19204,7 @@ ], "returns": "any" }, - "frequency_penalty": { + "from_number": { "params": [ { "name": "self", @@ -12863,7 +19213,7 @@ ], "returns": "any" }, - "presence_penalty": { + "region": { "params": [ { "name": "self", @@ -12872,7 +19222,7 @@ ], "returns": "any" }, - "temperature": { + "tags": { "params": [ { "name": "self", @@ -12881,7 +19231,7 @@ ], "returns": "any" }, - "top_p": { + "to_number": { "params": [ { "name": "self", @@ -12892,7 +19242,7 @@ } } }, - "OneOfProperty": { + "SMSWithMedia": { "methods": { "__init__": { "params": [ @@ -12903,7 +19253,7 @@ ], "returns": "void" }, - "oneOf": { + "body": { "params": [ { "name": "self", @@ -12911,21 +19261,17 @@ } ], "returns": "any" - } - } - }, - "Output": { - "methods": { - "__init__": { + }, + "from_number": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "action": { + "media": { "params": [ { "name": "self", @@ -12933,21 +19279,17 @@ } ], "returns": "any" - } - } - }, - "PayConfig": { - "methods": { - "__init__": { + }, + "region": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "max_attempts": { + "tags": { "params": [ { "name": "self", @@ -12956,7 +19298,7 @@ ], "returns": "any" }, - "min_postal_code_length": { + "to_number": { "params": [ { "name": "self", @@ -12964,8 +19306,21 @@ } ], "returns": "any" + } + } + }, + "SWAIG": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "parameters": { + "defaults": { "params": [ { "name": "self", @@ -12974,7 +19329,7 @@ ], "returns": "any" }, - "prompts": { + "functions": { "params": [ { "name": "self", @@ -12983,7 +19338,7 @@ ], "returns": "any" }, - "security_code": { + "includes": { "params": [ { "name": "self", @@ -12992,7 +19347,16 @@ ], "returns": "any" }, - "timeout": { + "internal_fillers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "native_functions": { "params": [ { "name": "self", @@ -13003,7 +19367,7 @@ } } }, - "PayPrompts": { + "SWAIGDefaults": { "methods": { "__init__": { "params": [ @@ -13014,7 +19378,7 @@ ], "returns": "void" }, - "actions": { + "web_hook_url": { "params": [ { "name": "self", @@ -13025,7 +19389,7 @@ } } }, - "PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "SWAIGIncludes": { "methods": { "__init__": { "params": [ @@ -13036,7 +19400,7 @@ ], "returns": "void" }, - "active": { + "functions": { "params": [ { "name": "self", @@ -13045,7 +19409,7 @@ ], "returns": "any" }, - "data_map": { + "meta_data": { "params": [ { "name": "self", @@ -13054,7 +19418,7 @@ ], "returns": "any" }, - "parameters": { + "url": { "params": [ { "name": "self", @@ -13065,7 +19429,7 @@ } } }, - "PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps": { + "SWAIGInternalFiller": { "methods": { "__init__": { "params": [ @@ -13076,7 +19440,7 @@ ], "returns": "void" }, - "active": { + "adjust_response_latency": { "params": [ { "name": "self", @@ -13085,7 +19449,7 @@ ], "returns": "any" }, - "data_map": { + "change_context": { "params": [ { "name": "self", @@ -13094,7 +19458,7 @@ ], "returns": "any" }, - "parameters": { + "check_time": { "params": [ { "name": "self", @@ -13102,21 +19466,17 @@ } ], "returns": "any" - } - } - }, - "PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps": { - "methods": { - "__init__": { + }, + "get_ideal_strategy": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "active": { + "get_visual_input": { "params": [ { "name": "self", @@ -13125,7 +19485,7 @@ ], "returns": "any" }, - "data_map": { + "hangup": { "params": [ { "name": "self", @@ -13134,7 +19494,7 @@ ], "returns": "any" }, - "parameters": { + "next_step": { "params": [ { "name": "self", @@ -13142,21 +19502,17 @@ } ], "returns": "any" - } - } - }, - "PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps": { - "methods": { - "__init__": { + }, + "wait_for_user": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "active": { + "wait_seconds": { "params": [ { "name": "self", @@ -13164,8 +19520,12 @@ } ], "returns": "any" - }, - "data_map": { + } + } + }, + "SWMLAction": { + "methods": { + "SWML": { "params": [ { "name": "self", @@ -13174,18 +19534,18 @@ ], "returns": "any" }, - "parameters": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" } } }, - "Play": { + "SayAction": { "methods": { "__init__": { "params": [ @@ -13196,7 +19556,7 @@ ], "returns": "void" }, - "play": { + "say": { "params": [ { "name": "self", @@ -13207,7 +19567,7 @@ } } }, - "PlayWithURL": { + "Section": { "methods": { "__init__": { "params": [ @@ -13218,7 +19578,7 @@ ], "returns": "void" }, - "auto_answer": { + "main": { "params": [ { "name": "self", @@ -13226,17 +19586,21 @@ } ], "returns": "any" - }, - "url": { + } + } + }, + "SendDigits": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "volume": { + "send_digits": { "params": [ { "name": "self", @@ -13247,7 +19611,7 @@ } } }, - "PlayWithURLS": { + "SendDigitsConfig": { "methods": { "__init__": { "params": [ @@ -13258,7 +19622,7 @@ ], "returns": "void" }, - "auto_answer": { + "digits": { "params": [ { "name": "self", @@ -13266,17 +19630,21 @@ } ], "returns": "any" - }, - "urls": { + } + } + }, + "SendFax": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "volume": { + "send_fax": { "params": [ { "name": "self", @@ -13287,7 +19655,7 @@ } } }, - "PomSectionBodyContent": { + "SendFaxConfig": { "methods": { "__init__": { "params": [ @@ -13298,7 +19666,7 @@ ], "returns": "void" }, - "numbered": { + "document": { "params": [ { "name": "self", @@ -13307,7 +19675,7 @@ ], "returns": "any" }, - "numberedBullets": { + "header_info": { "params": [ { "name": "self", @@ -13316,7 +19684,16 @@ ], "returns": "any" }, - "subsections": { + "identity": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "status_url": { "params": [ { "name": "self", @@ -13327,7 +19704,7 @@ } } }, - "PomSectionBulletsContent": { + "SendSMS": { "methods": { "__init__": { "params": [ @@ -13338,7 +19715,7 @@ ], "returns": "void" }, - "numbered": { + "send_sms": { "params": [ { "name": "self", @@ -13346,17 +19723,21 @@ } ], "returns": "any" - }, - "numberedBullets": { + } + } + }, + "Set": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "subsections": { + "set": { "params": [ { "name": "self", @@ -13367,7 +19748,7 @@ } } }, - "PromptConfig": { + "SetGlobalDataAction": { "methods": { "__init__": { "params": [ @@ -13378,7 +19759,7 @@ ], "returns": "void" }, - "digit_timeout": { + "set_global_data": { "params": [ { "name": "self", @@ -13386,17 +19767,21 @@ } ], "returns": "any" - }, - "initial_timeout": { + } + } + }, + "SetMetaDataAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "max_digits": { + "set_meta_data": { "params": [ { "name": "self", @@ -13404,8 +19789,21 @@ } ], "returns": "any" + } + } + }, + "SipReferConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "play": { + "password": { "params": [ { "name": "self", @@ -13414,7 +19812,7 @@ ], "returns": "any" }, - "speech_end_timeout": { + "status_url": { "params": [ { "name": "self", @@ -13423,7 +19821,7 @@ ], "returns": "any" }, - "speech_hints": { + "to_uri": { "params": [ { "name": "self", @@ -13432,7 +19830,7 @@ ], "returns": "any" }, - "speech_timeout": { + "username": { "params": [ { "name": "self", @@ -13443,7 +19841,7 @@ } } }, - "Pronounce": { + "Sleep": { "methods": { "__init__": { "params": [ @@ -13454,7 +19852,7 @@ ], "returns": "void" }, - "ignore_case": { + "sleep": { "params": [ { "name": "self", @@ -13465,7 +19863,7 @@ } } }, - "RecordCallConfig": { + "StartAction": { "methods": { "__init__": { "params": [ @@ -13476,7 +19874,7 @@ ], "returns": "void" }, - "beep": { + "start": { "params": [ { "name": "self", @@ -13484,17 +19882,21 @@ } ], "returns": "any" - }, - "end_silence_timeout": { + } + } + }, + "StartUpHookSWAIGFunction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "initial_timeout": { + "active": { "params": [ { "name": "self", @@ -13503,7 +19905,7 @@ ], "returns": "any" }, - "input_sensitivity": { + "argument": { "params": [ { "name": "self", @@ -13512,7 +19914,7 @@ ], "returns": "any" }, - "max_length": { + "data_map": { "params": [ { "name": "self", @@ -13521,7 +19923,7 @@ ], "returns": "any" }, - "stereo": { + "description": { "params": [ { "name": "self", @@ -13529,21 +19931,17 @@ } ], "returns": "any" - } - } - }, - "RecordConfig": { - "methods": { - "__init__": { + }, + "fillers": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "beep": { + "function": { "params": [ { "name": "self", @@ -13552,7 +19950,7 @@ ], "returns": "any" }, - "end_silence_timeout": { + "meta_data": { "params": [ { "name": "self", @@ -13561,7 +19959,7 @@ ], "returns": "any" }, - "initial_timeout": { + "meta_data_token": { "params": [ { "name": "self", @@ -13570,7 +19968,7 @@ ], "returns": "any" }, - "input_sensitivity": { + "parameters": { "params": [ { "name": "self", @@ -13579,7 +19977,7 @@ ], "returns": "any" }, - "max_length": { + "purpose": { "params": [ { "name": "self", @@ -13588,7 +19986,7 @@ ], "returns": "any" }, - "stereo": { + "skip_fillers": { "params": [ { "name": "self", @@ -13596,21 +19994,17 @@ } ], "returns": "any" - } - } - }, - "RequestConfig": { - "methods": { - "__init__": { + }, + "wait_file": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "connect_timeout": { + "wait_file_loops": { "params": [ { "name": "self", @@ -13619,7 +20013,7 @@ ], "returns": "any" }, - "save_variables": { + "wait_for_fillers": { "params": [ { "name": "self", @@ -13628,7 +20022,7 @@ ], "returns": "any" }, - "timeout": { + "web_hook_url": { "params": [ { "name": "self", @@ -13639,7 +20033,7 @@ } } }, - "SWAIG": { + "StopAction": { "methods": { "__init__": { "params": [ @@ -13650,7 +20044,7 @@ ], "returns": "void" }, - "defaults": { + "stop": { "params": [ { "name": "self", @@ -13658,17 +20052,21 @@ } ], "returns": "any" - }, - "functions": { + } + } + }, + "StopDenoise": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "includes": { + "stop_denoise": { "params": [ { "name": "self", @@ -13676,17 +20074,21 @@ } ], "returns": "any" - }, - "internal_fillers": { + } + } + }, + "StopPlaybackBGAction": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "native_functions": { + "stop_playback_bg": { "params": [ { "name": "self", @@ -13697,7 +20099,7 @@ } } }, - "SWAIGInternalFiller": { + "StopRecordCall": { "methods": { "__init__": { "params": [ @@ -13708,7 +20110,7 @@ ], "returns": "void" }, - "adjust_response_latency": { + "stop_record_call": { "params": [ { "name": "self", @@ -13716,17 +20118,21 @@ } ], "returns": "any" - }, - "change_context": { + } + } + }, + "StopRecordCallConfig": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "check_time": { + "control_id": { "params": [ { "name": "self", @@ -13734,17 +20140,21 @@ } ], "returns": "any" - }, - "get_ideal_strategy": { + } + } + }, + "StopTap": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "any" + "returns": "void" }, - "get_visual_input": { + "stop_tap": { "params": [ { "name": "self", @@ -13752,8 +20162,21 @@ } ], "returns": "any" + } + } + }, + "StopTapConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "hangup": { + "control_id": { "params": [ { "name": "self", @@ -13761,8 +20184,21 @@ } ], "returns": "any" + } + } + }, + "StringProperty": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "next_step": { + "default": { "params": [ { "name": "self", @@ -13771,7 +20207,7 @@ ], "returns": "any" }, - "wait_for_user": { + "description": { "params": [ { "name": "self", @@ -13780,7 +20216,7 @@ ], "returns": "any" }, - "wait_seconds": { + "enum": { "params": [ { "name": "self", @@ -13788,21 +20224,17 @@ } ], "returns": "any" - } - } - }, - "Section": { - "methods": { - "__init__": { + }, + "format": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "main": { + "nullable": { "params": [ { "name": "self", @@ -13810,21 +20242,17 @@ } ], "returns": "any" - } - } - }, - "SendSMS": { - "methods": { - "__init__": { + }, + "pattern": { "params": [ { "name": "self", "kind": "self" } ], - "returns": "void" + "returns": "any" }, - "send_sms": { + "type": { "params": [ { "name": "self", @@ -13835,7 +20263,7 @@ } } }, - "Sleep": { + "SummarizeAction": { "methods": { "__init__": { "params": [ @@ -13846,7 +20274,7 @@ ], "returns": "void" }, - "sleep": { + "summarize": { "params": [ { "name": "self", @@ -13857,7 +20285,7 @@ } } }, - "StartUpHookSWAIGFunction": { + "SummarizeConversationSWAIGFunction": { "methods": { "__init__": { "params": [ @@ -13895,6 +20323,15 @@ ], "returns": "any" }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "fillers": { "params": [ { @@ -13904,6 +20341,33 @@ ], "returns": "any" }, + "function": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data_token": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "parameters": { "params": [ { @@ -13913,6 +20377,15 @@ ], "returns": "any" }, + "purpose": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "skip_fillers": { "params": [ { @@ -13922,6 +20395,24 @@ ], "returns": "any" }, + "wait_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait_file_loops": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "wait_for_fillers": { "params": [ { @@ -13930,10 +20421,19 @@ } ], "returns": "any" + }, + "web_hook_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" } } }, - "StopAction": { + "Switch": { "methods": { "__init__": { "params": [ @@ -13944,7 +20444,7 @@ ], "returns": "void" }, - "stop": { + "switch": { "params": [ { "name": "self", @@ -13955,7 +20455,7 @@ } } }, - "StopPlaybackBGAction": { + "SwitchConfig": { "methods": { "__init__": { "params": [ @@ -13966,7 +20466,25 @@ ], "returns": "void" }, - "stop_playback_bg": { + "case": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "default": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "variable": { "params": [ { "name": "self", @@ -13977,7 +20495,7 @@ } } }, - "StringProperty": { + "Tap": { "methods": { "__init__": { "params": [ @@ -13988,7 +20506,7 @@ ], "returns": "void" }, - "format": { + "tap": { "params": [ { "name": "self", @@ -13996,8 +20514,66 @@ } ], "returns": "any" + } + } + }, + "TapConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "nullable": { + "codec": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "control_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "direction": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "rtp_ptime": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "status_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "uri": { "params": [ { "name": "self", @@ -14008,7 +20584,7 @@ } } }, - "SummarizeConversationSWAIGFunction": { + "ToggleFunctionsAction": { "methods": { "__init__": { "params": [ @@ -14019,7 +20595,7 @@ ], "returns": "void" }, - "active": { + "toggle_functions": { "params": [ { "name": "self", @@ -14027,8 +20603,21 @@ } ], "returns": "any" + } + } + }, + "TranscribeStartAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "argument": { + "start": { "params": [ { "name": "self", @@ -14036,8 +20625,43 @@ } ], "returns": "any" + } + } + }, + "TranscribeSummarizeAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "data_map": { + "summarize": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "Transfer": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "transfer": { "params": [ { "name": "self", @@ -14045,8 +20669,21 @@ } ], "returns": "any" + } + } + }, + "TransferConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "fillers": { + "dest": { "params": [ { "name": "self", @@ -14055,7 +20692,7 @@ ], "returns": "any" }, - "parameters": { + "meta": { "params": [ { "name": "self", @@ -14064,7 +20701,7 @@ ], "returns": "any" }, - "skip_fillers": { + "params": { "params": [ { "name": "self", @@ -14072,8 +20709,21 @@ } ], "returns": "any" + } + } + }, + "Unset": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" }, - "wait_for_fillers": { + "unset": { "params": [ { "name": "self", @@ -14084,7 +20734,7 @@ } } }, - "SwitchConfig": { + "UnsetGlobalDataAction": { "methods": { "__init__": { "params": [ @@ -14095,7 +20745,7 @@ ], "returns": "void" }, - "default": { + "unset_global_data": { "params": [ { "name": "self", @@ -14106,7 +20756,7 @@ } } }, - "TapConfig": { + "UnsetMetaDataAction": { "methods": { "__init__": { "params": [ @@ -14117,7 +20767,73 @@ ], "returns": "void" }, - "rtp_ptime": { + "unset_meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "UserEvent": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "user_event": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "UserEventConfig": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "event": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + } + } + }, + "UserInputAction": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "user_input": { "params": [ { "name": "self", @@ -14166,6 +20882,15 @@ ], "returns": "any" }, + "description": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "fillers": { "params": [ { @@ -14175,6 +20900,33 @@ ], "returns": "any" }, + "function": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "meta_data_token": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "parameters": { "params": [ { @@ -14184,6 +20936,15 @@ ], "returns": "any" }, + "purpose": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "skip_fillers": { "params": [ { @@ -14193,6 +20954,24 @@ ], "returns": "any" }, + "wait_file": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "wait_file_loops": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "wait_for_fillers": { "params": [ { @@ -14201,6 +20980,15 @@ } ], "returns": "any" + }, + "web_hook_url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" } } }, @@ -14215,6 +21003,15 @@ ], "returns": "void" }, + "error_keys": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "expressions": { "params": [ { @@ -14224,6 +21021,24 @@ ], "returns": "any" }, + "foreach": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "headers": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "input_args_as_params": { "params": [ { @@ -14233,6 +21048,15 @@ ], "returns": "any" }, + "method": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, "output": { "params": [ { @@ -14241,6 +21065,33 @@ } ], "returns": "any" + }, + "params": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "require_args": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "url": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" } } } @@ -14518,7 +21369,7 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", @@ -14536,7 +21387,7 @@ "name": "numbered_bullets", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.pom.pom.Section" @@ -14694,7 +21545,7 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "bullets", @@ -14704,15 +21555,15 @@ }, { "name": "numbered", - "type": "optional", + "type": "bool", "required": false, - "default": null + "default": false }, { "name": "numbered_bullets", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "class:signalwire.pom.pom.Section" @@ -14763,7 +21614,7 @@ "name": "level", "type": "int", "required": false, - "default": null + "default": 2 }, { "name": "section_number", @@ -14784,7 +21635,7 @@ "name": "indent", "type": "int", "required": false, - "default": null + "default": 0 }, { "name": "section_number", @@ -14849,25 +21700,25 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "concierge" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/concierge" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 } ], "returns": "void" @@ -15001,25 +21852,25 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "faq_bot" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/faq" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 } ], "returns": "void" @@ -15116,25 +21967,25 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "info_gatherer" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/info_gatherer" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 } ], "returns": "void" @@ -15147,21 +21998,24 @@ }, { "name": "request_data", - "type": "any", - "required": true + "type": "optional", + "required": false, + "default": null }, { - "name": "query_params", - "type": "any", - "required": true + "name": "callback_path", + "type": "optional", + "required": false, + "default": null }, { - "name": "headers", - "type": "any", - "required": true + "name": "request", + "type": "optional", + "required": false, + "default": null } ], - "returns": "any" + "returns": "optional" }, "set_completion_message": { "params": [ @@ -15275,25 +22129,25 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "receptionist" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/receptionist" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 } ], "returns": "void" @@ -15372,25 +22226,25 @@ "name": "name", "type": "string", "required": false, - "default": null + "default": "survey" }, { "name": "route", "type": "string", "required": false, - "default": null + "default": "/survey" }, { "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "port", "type": "int", "required": false, - "default": null + "default": 3000 } ], "returns": "void" @@ -15662,7 +22516,7 @@ "name": "final_state", "type": "string", "required": false, - "default": null + "default": "finished" }, { "name": "result", @@ -15880,6 +22734,15 @@ ], "returns": "class:signalwire.relay.call.Call" }, + "completed": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, "control_id": { "params": [ { @@ -15971,8 +22834,16 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "prompt", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -15986,8 +22857,30 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "message_text", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "role", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "reset", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "global_data", + "kind": "keyword", + "type": "optional>", "required": false, "default": null } @@ -16001,8 +22894,9 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "prompt", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16016,8 +22910,44 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "prompt", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "SWAIG", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "ai_params", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "global_data", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "post_prompt", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "post_prompt_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16050,8 +22980,23 @@ "required": true }, { - "name": "params", - "type": "any", + "name": "bind_params", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "realm", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "max_triggers", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16185,13 +23130,50 @@ "kind": "self" }, { - "name": "amd_params", - "type": "any", + "name": "initial_timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "end_silence_timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "machine_voice_threshold", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "machine_words_threshold", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "detect_interruptions", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "detect_message_end", + "kind": "keyword", + "type": "optional", "required": false, "default": null }, { "name": "timeout", + "kind": "keyword", "type": "float", "required": false, "default": null @@ -16289,8 +23271,16 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "timeout", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16344,7 +23334,7 @@ "name": "reason", "type": "string", "required": false, - "default": null + "default": "hangup" } ], "returns": "class:signalwire.relay.action.Action" @@ -16419,8 +23409,7 @@ { "name": "conference_id", "type": "string", - "required": false, - "default": null + "required": true } ], "returns": "class:signalwire.relay.action.Action" @@ -16692,31 +23681,6 @@ ], "returns": "string" }, - "prompt": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "play_media", - "type": "any", - "required": true - }, - { - "name": "collect_params", - "type": "any", - "required": true - }, - { - "name": "control_id", - "type": "string", - "required": false, - "default": null - } - ], - "returns": "class:signalwire.relay.action.Action" - }, "prompt_audio": { "params": [ { @@ -16797,8 +23761,16 @@ "required": true }, { - "name": "params", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16817,8 +23789,23 @@ "required": true }, { - "name": "params", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "queue_id", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "status_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16888,8 +23875,9 @@ "required": true }, { - "name": "params", - "type": "any", + "name": "status_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -16925,7 +23913,7 @@ "name": "final_state", "type": "string", "required": false, - "default": null + "default": "finished" } ], "returns": "void" @@ -17128,7 +24116,7 @@ "name": "control_id", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.relay.action.Action" @@ -17149,14 +24137,16 @@ "kind": "self" }, { - "name": "params", - "type": "any", + "name": "control_id", + "kind": "keyword", + "type": "string", "required": false, "default": null }, { - "name": "control_id", - "type": "string", + "name": "status_url", + "kind": "keyword", + "type": "optional", "required": false, "default": null } @@ -17652,14 +24642,14 @@ "default": null }, { - "name": "dial_timeout_ms", + "name": "max_duration", "type": "int", "required": false, "default": null }, { - "name": "max_duration", - "type": "int", + "name": "dial_timeout", + "type": "optional", "required": false, "default": null } @@ -19841,13 +26831,13 @@ "name": "code", "type": "int", "required": false, - "default": null + "default": 1000 }, { "name": "reason", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "void" @@ -19867,7 +26857,7 @@ "name": "port", "type": "int", "required": false, - "default": null + "default": 443 } ], "returns": "bool" @@ -20016,7 +27006,7 @@ "name": "update_method", "type": "string", "required": false, - "default": null + "default": "PATCH" } ], "returns": "void" @@ -20085,7 +27075,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "any" @@ -20107,7 +27097,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "any" @@ -20162,7 +27152,7 @@ "name": "update_method", "type": "string", "required": false, - "default": null + "default": "PATCH" } ], "returns": "void" @@ -20189,7 +27179,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "any" @@ -20453,7 +27443,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "any" @@ -20475,7 +27465,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "any" @@ -20497,7 +27487,7 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "class:signalwire.rest._pagination.PaginatedIterator" @@ -20526,25 +27516,25 @@ "name": "body", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "url", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "method", "type": "string", "required": false, - "default": null + "default": "GET" }, { "name": "headers", "type": "dict", "required": false, - "default": null + "default": {} } ], "returns": "void" @@ -20622,13 +27612,13 @@ "name": "url", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "method", "type": "string", "required": false, - "default": null + "default": "GET" } ], "returns": "void" @@ -20661,13 +27651,13 @@ "name": "params", "type": "dict", "required": false, - "default": null + "default": {} }, { "name": "data_key", "type": "string", "required": false, - "default": null + "default": "data" }, { "name": "request_options", @@ -20714,6 +27704,15 @@ ], "returns": "class:signalwire.rest._base.HttpClient" }, + "index": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, "items": { "params": [ { @@ -24459,25 +31458,441 @@ ], "returns": "any" }, - "refresh_subscriber_token": { + "refresh_subscriber_token": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "refresh_token", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "FreeswitchConnectors": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "GenericResources": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "assign_domain_application": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "domain_application_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "assign_phone_route": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "phone_route_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "handler", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_addresses": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + }, + "RelayApplications": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + } + } + }, + "SipEndpoints": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "refresh_token", - "kind": "keyword", - "type": "dict", + "name": "id", + "kind": "positional", + "type": "string", "required": true }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -24486,18 +31901,17 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" } } }, - "FreeswitchConnectors": { + "SipGateways": { "methods": { "__init__": { "params": [ @@ -24565,7 +31979,7 @@ } } }, - "GenericResources": { + "Subscribers": { "methods": { "__init__": { "params": [ @@ -24581,31 +31995,12 @@ ], "returns": "void" }, - "assign_domain_application": { + "create": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "domain_application_id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -24614,39 +32009,68 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" }, - "assign_phone_route": { + "create_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "fabric_subscriber_id", "kind": "positional", "type": "string", "required": true }, { - "name": "phone_route_id", + "name": "username", "kind": "keyword", - "type": "dict", + "type": "string", "required": true }, { - "name": "handler", + "name": "password", "kind": "keyword", - "type": "dict", + "type": "string", "required": true }, + { + "name": "caller_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, { "name": "extras", "kind": "keyword", @@ -24671,12 +32095,18 @@ ], "returns": "any" }, - "delete": { + "delete_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "id", "kind": "positional", @@ -24693,12 +32123,18 @@ ], "returns": "any" }, - "get": { + "get_sip_endpoint": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "id", "kind": "positional", @@ -24722,12 +32158,18 @@ ], "returns": "any" }, - "list": { + "list_sip_endpoints": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -24745,7 +32187,7 @@ ], "returns": "any" }, - "list_addresses": { + "update": { "params": [ { "name": "self", @@ -24765,7 +32207,90 @@ "default": null }, { - "name": "params", + "name": "body", + "kind": "positional", + "type": "dict", + "required": true + } + ], + "returns": "any" + }, + "update_sip_endpoint": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "fabric_subscriber_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "username", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "password", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "caller_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -24776,7 +32301,7 @@ } } }, - "RelayApplications": { + "SwmlScripts": { "methods": { "__init__": { "params": [ @@ -24844,7 +32369,7 @@ } } }, - "SipEndpoints": { + "SwmlWebhooks": { "methods": { "__init__": { "params": [ @@ -24911,8 +32436,12 @@ "returns": "any" } } - }, - "SipGateways": { + } + } + }, + "signalwire.rest.namespaces.fax_resources_generated": { + "classes": { + "FaxLogs": { "methods": { "__init__": { "params": [ @@ -24928,12 +32457,18 @@ ], "returns": "void" }, - "create": { + "get": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -24942,25 +32477,43 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "update": { + "list": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" }, { "name": "request_options", @@ -24970,17 +32523,22 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" } } - }, - "Subscribers": { + } + } + }, + "signalwire.rest.namespaces.logs_resources_generated": { + "classes": { + "ConferenceLogs": { "methods": { "__init__": { "params": [ @@ -24996,7 +32554,7 @@ ], "returns": "void" }, - "create": { + "list": { "params": [ { "name": "self", @@ -25010,64 +32568,178 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.message_resources_generated": { + "classes": { + "MessageLogs": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, - "create_sip_endpoint": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fabric_subscriber_id", + "name": "id", "kind": "positional", "type": "string", "required": true }, { - "name": "username", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "paginate": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.messages_resources_generated": { + "classes": { + "Messages": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "to", "kind": "keyword", "type": "string", "required": true }, { - "name": "password", + "name": "from", "kind": "keyword", "type": "string", "required": true }, { - "name": "caller_id", + "name": "body", "kind": "keyword", "type": "optional", "required": false }, { - "name": "send_as", + "name": "media", "kind": "keyword", "type": "optional", "required": false }, { - "name": "ciphers", + "name": "send_as_mms", "kind": "keyword", "type": "optional", "required": false }, { - "name": "codecs", + "name": "status_callback", "kind": "keyword", "type": "optional", "required": false }, { - "name": "encryption", + "name": "custom_variables", "kind": "keyword", "type": "optional", "required": false @@ -25096,51 +32768,30 @@ ], "returns": "any" }, - "delete_sip_endpoint": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fabric_subscriber_id", + "name": "message_id", "kind": "positional", "type": "string", "required": true }, { - "name": "id", - "kind": "positional", + "name": "body", + "kind": "keyword", "type": "string", "required": true }, { - "name": "request_options", + "name": "extras", "kind": "keyword", - "type": "optional", + "type": "optional>", "required": false, "default": null - } - ], - "returns": "any" - }, - "get_sip_endpoint": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true }, { "name": "request_options", @@ -25150,7 +32801,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -25158,19 +32809,60 @@ } ], "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.project_resources_generated": { + "classes": { + "ProjectTokens": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, - "list_sip_endpoints": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fabric_subscriber_id", - "kind": "positional", + "name": "name", + "kind": "keyword", "type": "string", "required": true }, + { + "name": "permissions", + "kind": "keyword", + "type": "list", + "required": true + }, + { + "name": "subproject_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -25179,7 +32871,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -25188,14 +32880,14 @@ ], "returns": "any" }, - "update": { + "delete": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "token_id", "kind": "positional", "type": "string", "required": true @@ -25206,72 +32898,30 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "update_sip_endpoint": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "fabric_subscriber_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "id", + "name": "token_id", "kind": "positional", "type": "string", "required": true }, { - "name": "username", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "password", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "caller_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "send_as", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "ciphers", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "codecs", + "name": "name", "kind": "keyword", "type": "optional", "required": false }, { - "name": "encryption", + "name": "permissions", "kind": "keyword", "type": "optional", "required": false @@ -25301,8 +32951,12 @@ "returns": "any" } } - }, - "SwmlScripts": { + } + } + }, + "signalwire.rest.namespaces.projects_resources_generated": { + "classes": { + "Projects": { "methods": { "__init__": { "params": [ @@ -25340,7 +32994,7 @@ ], "returns": "any" }, - "update": { + "delete": { "params": [ { "name": "self", @@ -25358,39 +33012,21 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" - } - } - }, - "SwmlWebhooks": { - "methods": { - "__init__": { + }, + "rotate_signing_key": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ - { - "name": "self", - "kind": "self" }, { "name": "request_options", @@ -25398,12 +33034,6 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" @@ -25440,9 +33070,9 @@ } } }, - "signalwire.rest.namespaces.fax_resources_generated": { + "signalwire.rest.namespaces.pubsub_resources_generated": { "classes": { - "FaxLogs": { + "PubSub": { "methods": { "__init__": { "params": [ @@ -25458,63 +33088,42 @@ ], "returns": "void" }, - "get": { + "create_token": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", - "type": "string", + "name": "ttl", + "kind": "keyword", + "type": "int", "required": true }, { - "name": "request_options", + "name": "channels", "kind": "keyword", - "type": "optional", - "required": false, - "default": null + "type": "dict", + "required": true }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list": { - "params": [ - { - "name": "self", - "kind": "self" + "name": "member_id", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "request_options", + "name": "state", "kind": "keyword", - "type": "optional", - "required": false, - "default": null + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", + "name": "extras", + "kind": "keyword", + "type": "optional>", "required": false, - "default": {} - } - ], - "returns": "any" - }, - "paginate": { - "params": [ - { - "name": "self", - "kind": "self" + "default": null }, { "name": "request_options", @@ -25524,7 +33133,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -25537,9 +33146,9 @@ } } }, - "signalwire.rest.namespaces.logs_resources_generated": { + "signalwire.rest.namespaces.relay_rest_resources_generated": { "classes": { - "ConferenceLogs": { + "Addresses": { "methods": { "__init__": { "params": [ @@ -25555,12 +33164,85 @@ ], "returns": "void" }, - "list": { + "create": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "label", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "country", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "first_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "last_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "street_number", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "street_name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "city", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "state", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "postal_code", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "address_type", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "address_number", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -25569,7 +33251,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -25577,30 +33259,8 @@ } ], "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.message_resources_generated": { - "classes": { - "MessageLogs": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, - "get": { + "delete": { "params": [ { "name": "self", @@ -25618,23 +33278,22 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "list": { + "get": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -25652,7 +33311,7 @@ ], "returns": "any" }, - "paginate": { + "list": { "params": [ { "name": "self", @@ -25676,12 +33335,8 @@ "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.messages_resources_generated": { - "classes": { - "Messages": { + }, + "ImportedNumbers": { "methods": { "__init__": { "params": [ @@ -25704,43 +33359,19 @@ "kind": "self" }, { - "name": "to", + "name": "number", "kind": "keyword", "type": "string", "required": true }, { - "name": "from", + "name": "number_type", "kind": "keyword", "type": "string", "required": true }, { - "name": "body", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "media", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "send_as_mms", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_callback", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "custom_variables", + "name": "capabilities", "kind": "keyword", "type": "optional", "required": false @@ -25768,32 +33399,37 @@ } ], "returns": "any" - }, - "update": { + } + } + }, + "Lookup": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "message_id", - "kind": "positional", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true + } + ], + "returns": "void" + }, + "phone_number": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "body", - "kind": "keyword", + "name": "e164", + "kind": "positional", "type": "string", "required": true }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -25802,7 +33438,7 @@ "default": null }, { - "name": "kwargs", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -25812,12 +33448,8 @@ "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.project_resources_generated": { - "classes": { - "ProjectTokens": { + }, + "Mfa": { "methods": { "__init__": { "params": [ @@ -25833,26 +33465,50 @@ ], "returns": "void" }, - "create": { + "call": { "params": [ { "name": "self", "kind": "self" }, { - "name": "name", + "name": "to", "kind": "keyword", "type": "string", "required": true }, { - "name": "permissions", + "name": "from", "kind": "keyword", - "type": "list", - "required": true + "type": "optional", + "required": false }, { - "name": "subproject_id", + "name": "message", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "token_length", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "valid_for", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "max_attempts", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "allow_alphas", "kind": "keyword", "type": "optional", "required": false @@ -25881,51 +33537,95 @@ ], "returns": "any" }, - "delete": { + "sms": { "params": [ { "name": "self", "kind": "self" }, { - "name": "token_id", - "kind": "positional", + "name": "to", + "kind": "keyword", "type": "string", "required": true }, + { + "name": "from", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "token_length", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "valid_for", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "max_attempts", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "allow_alphas", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", "type": "optional", "required": false, "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "update": { + "verify": { "params": [ { "name": "self", "kind": "self" }, { - "name": "token_id", + "name": "mfa_request_id", "kind": "positional", "type": "string", "required": true }, { - "name": "name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "permissions", + "name": "token", "kind": "keyword", - "type": "optional", - "required": false + "type": "string", + "required": true }, { "name": "extras", @@ -25952,12 +33652,8 @@ "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.projects_resources_generated": { - "classes": { - "Projects": { + }, + "NumberGroups": { "methods": { "__init__": { "params": [ @@ -25966,12 +33662,54 @@ "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" + }, + "add_membership": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "NumberGroupId", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "phone_number_id", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "void" + "returns": "any" }, "create": { "params": [ @@ -26017,7 +33755,7 @@ ], "returns": "any" }, - "rotate_signing_key": { + "delete_membership": { "params": [ { "name": "self", @@ -26039,7 +33777,7 @@ ], "returns": "any" }, - "update": { + "get_membership": { "params": [ { "name": "self", @@ -26059,73 +33797,56 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" - } - } - } - } - }, - "signalwire.rest.namespaces.pubsub_resources_generated": { - "classes": { - "PubSub": { - "methods": { - "__init__": { + }, + "list_memberships": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "NumberGroupId", + "kind": "positional", + "type": "string", "required": true + }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], - "returns": "void" + "returns": "any" }, - "create_token": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "ttl", - "kind": "keyword", - "type": "int", - "required": true - }, - { - "name": "channels", - "kind": "keyword", - "type": "dict", + "name": "id", + "kind": "positional", + "type": "string", "required": true }, - { - "name": "member_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "state", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26134,22 +33855,17 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.relay_rest_resources_generated": { - "classes": { - "Addresses": { + }, + "PhoneNumbers": { "methods": { "__init__": { "params": [ @@ -26171,79 +33887,6 @@ "name": "self", "kind": "self" }, - { - "name": "label", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "country", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "first_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "last_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "street_number", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "street_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "city", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "state", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "postal_code", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "address_type", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "address_number", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26252,11 +33895,10 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" @@ -26283,18 +33925,12 @@ ], "returns": "any" }, - "get": { + "search": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, { "name": "request_options", "kind": "keyword", @@ -26312,12 +33948,24 @@ ], "returns": "any" }, - "list": { + "set_ai_agent": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true + }, + { + "name": "agent_id", + "kind": "positional", + "type": "dict", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -26326,7 +33974,7 @@ "default": null }, { - "name": "params", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26334,56 +33982,31 @@ } ], "returns": "any" - } - } - }, - "ImportedNumbers": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, - "create": { + "set_call_flow": { "params": [ { "name": "self", "kind": "self" }, { - "name": "number", - "kind": "keyword", + "name": "resource_id", + "kind": "positional", "type": "string", "required": true }, { - "name": "number_type", - "kind": "keyword", - "type": "string", + "name": "flow_id", + "kind": "positional", + "type": "dict", "required": true }, { - "name": "capabilities", - "kind": "keyword", + "name": "version", + "kind": "positional", "type": "optional", "required": false }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26392,7 +34015,7 @@ "default": null }, { - "name": "kwargs", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26400,33 +34023,21 @@ } ], "returns": "any" - } - } - }, - "Lookup": { - "methods": { - "__init__": { + }, + "set_cxml_application": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "resource_id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "phone_number": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "e164", + "name": "application_id", "kind": "positional", "type": "string", "required": true @@ -26439,7 +34050,7 @@ "default": null }, { - "name": "params", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26447,79 +34058,71 @@ } ], "returns": "any" - } - } - }, - "Mfa": { - "methods": { - "__init__": { + }, + "set_cxml_webhook": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "resource_id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "call": { - "params": [ - { - "name": "self", - "kind": "self" }, { - "name": "to", - "kind": "keyword", + "name": "url", + "kind": "positional", "type": "string", "required": true }, { - "name": "from", - "kind": "keyword", + "name": "fallback_url", + "kind": "positional", "type": "optional", "required": false }, { - "name": "message", - "kind": "keyword", + "name": "status_callback_url", + "kind": "positional", "type": "optional", "required": false }, { - "name": "token_length", + "name": "request_options", "kind": "keyword", - "type": "optional", - "required": false + "type": "optional", + "required": false, + "default": null }, { - "name": "valid_for", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "set_relay_application": { + "params": [ { - "name": "max_attempts", - "kind": "keyword", - "type": "optional", - "required": false + "name": "self", + "kind": "self" }, { - "name": "allow_alphas", - "kind": "keyword", - "type": "optional", - "required": false + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": true }, { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null + "name": "name", + "kind": "positional", + "type": "string", + "required": true }, { "name": "request_options", @@ -26529,7 +34132,7 @@ "default": null }, { - "name": "kwargs", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26538,61 +34141,30 @@ ], "returns": "any" }, - "sms": { + "set_relay_topic": { "params": [ { "name": "self", "kind": "self" }, { - "name": "to", - "kind": "keyword", + "name": "resource_id", + "kind": "positional", "type": "string", "required": true }, { - "name": "from", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "token_length", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "valid_for", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "max_attempts", - "kind": "keyword", - "type": "optional", - "required": false + "name": "topic", + "kind": "positional", + "type": "string", + "required": true }, { - "name": "allow_alphas", - "kind": "keyword", + "name": "status_callback_url", + "kind": "positional", "type": "optional", "required": false }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26601,7 +34173,7 @@ "default": null }, { - "name": "kwargs", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26610,31 +34182,24 @@ ], "returns": "any" }, - "verify": { + "set_swml_webhook": { "params": [ { "name": "self", "kind": "self" }, { - "name": "mfa_request_id", + "name": "resource_id", "kind": "positional", "type": "string", "required": true }, { - "name": "token", - "kind": "keyword", + "name": "url", + "kind": "positional", "type": "string", "required": true }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26643,7 +34208,7 @@ "default": null }, { - "name": "kwargs", + "name": "extra", "kind": "var_keyword", "type": "any", "required": false, @@ -26651,50 +34216,19 @@ } ], "returns": "any" - } - } - }, - "NumberGroups": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, - "add_membership": { + "update": { "params": [ { "name": "self", "kind": "self" }, { - "name": "NumberGroupId", + "name": "id", "kind": "positional", "type": "string", "required": true }, - { - "name": "phone_number_id", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -26703,14 +34237,31 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" + } + } + }, + "Queues": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, "create": { "params": [ @@ -26756,12 +34307,18 @@ ], "returns": "any" }, - "delete_membership": { + "get_member": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "queue_id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "id", "kind": "positional", @@ -26773,19 +34330,26 @@ "kind": "keyword", "type": "optional", "required": false, - "default": null + "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "get_membership": { + "get_next_member": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", + "name": "queue_id", "kind": "positional", "type": "string", "required": true @@ -26807,14 +34371,14 @@ ], "returns": "any" }, - "list_memberships": { + "list_members": { "params": [ { "name": "self", "kind": "self" }, { - "name": "NumberGroupId", + "name": "queue_id", "kind": "positional", "type": "string", "required": true @@ -26866,7 +34430,7 @@ } } }, - "PhoneNumbers": { + "Recordings": { "methods": { "__init__": { "params": [ @@ -26882,29 +34446,29 @@ ], "returns": "void" }, - "create": { + "delete": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", "type": "optional", "required": false, "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], "returns": "any" }, - "delete": { + "get": { "params": [ { "name": "self", @@ -26922,11 +34486,18 @@ "type": "optional", "required": false, "default": null + }, + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "search": { + "list": { "params": [ { "name": "self", @@ -26948,21 +34519,33 @@ } ], "returns": "any" - }, - "set_ai_agent": { + } + } + }, + "RegistryBrands": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true + } + ], + "returns": "void" + }, + "create": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "agent_id", + "name": "body", "kind": "positional", "type": "dict", "required": true @@ -26973,40 +34556,49 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "extra", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "set_call_flow": { + "create_campaign": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", + "name": "id", "kind": "positional", "type": "string", "required": true }, { - "name": "flow_id", + "name": "body", "kind": "positional", "type": "dict", "required": true }, { - "name": "version", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", "kind": "positional", - "type": "optional", - "required": false + "type": "string", + "required": true }, { "name": "request_options", @@ -27016,7 +34608,7 @@ "default": null }, { - "name": "extra", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27025,20 +34617,37 @@ ], "returns": "any" }, - "set_cxml_application": { + "list": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null }, { - "name": "application_id", + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} + } + ], + "returns": "any" + }, + "list_campaigns": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", "kind": "positional", "type": "string", "required": true @@ -27051,7 +34660,7 @@ "default": null }, { - "name": "extra", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27059,37 +34668,56 @@ } ], "returns": "any" - }, - "set_cxml_webhook": { + } + } + }, + "RegistryCampaigns": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true + } + ], + "returns": "void" + }, + "create_order": { + "params": [ + { + "name": "self", + "kind": "self" }, { - "name": "url", + "name": "id", "kind": "positional", "type": "string", "required": true }, { - "name": "fallback_url", - "kind": "positional", + "name": "phone_numbers", + "kind": "keyword", "type": "optional", "required": false }, { "name": "status_callback_url", - "kind": "positional", + "kind": "keyword", "type": "optional", "required": false }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -27098,7 +34726,7 @@ "default": null }, { - "name": "extra", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -27107,20 +34735,14 @@ ], "returns": "any" }, - "set_relay_application": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "name", + "name": "id", "kind": "positional", "type": "string", "required": true @@ -27133,7 +34755,7 @@ "default": null }, { - "name": "extra", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27142,30 +34764,18 @@ ], "returns": "any" }, - "set_relay_topic": { + "list_numbers": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "topic", + "name": "id", "kind": "positional", "type": "string", "required": true }, - { - "name": "status_callback_url", - "kind": "positional", - "type": "optional", - "required": false - }, { "name": "request_options", "kind": "keyword", @@ -27174,7 +34784,7 @@ "default": null }, { - "name": "extra", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27183,20 +34793,14 @@ ], "returns": "any" }, - "set_swml_webhook": { + "list_orders": { "params": [ { "name": "self", "kind": "self" }, { - "name": "resource_id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "url", + "name": "id", "kind": "positional", "type": "string", "required": true @@ -27209,7 +34813,7 @@ "default": null }, { - "name": "extra", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27230,6 +34834,19 @@ "type": "string", "required": true }, + { + "name": "name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -27238,17 +34855,18 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" } } }, - "Queues": { + "RegistryNumbers": { "methods": { "__init__": { "params": [ @@ -27264,28 +34882,6 @@ ], "returns": "void" }, - "create": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true - } - ], - "returns": "any" - }, "delete": { "params": [ { @@ -27307,18 +34903,30 @@ } ], "returns": "any" - }, - "get_member": { + } + } + }, + "RegistryOrders": { + "methods": { + "__init__": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_id", - "kind": "positional", - "type": "string", + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", "required": true + } + ], + "returns": "void" + }, + "get": { + "params": [ + { + "name": "self", + "kind": "self" }, { "name": "id", @@ -27342,15 +34950,33 @@ } ], "returns": "any" + } + } + }, + "ShortCodes": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, - "get_next_member": { + "get": { "params": [ { "name": "self", "kind": "self" }, { - "name": "queue_id", + "name": "id", "kind": "positional", "type": "string", "required": true @@ -27372,18 +34998,12 @@ ], "returns": "any" }, - "list_members": { + "list": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "queue_id", - "kind": "positional", - "type": "string", - "required": true - }, { "name": "request_options", "kind": "keyword", @@ -27413,6 +35033,61 @@ "type": "string", "required": true }, + { + "name": "name", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "message_handler", + "kind": "keyword", + "type": "dict", + "required": true + }, + { + "name": "message_request_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_request_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_fallback_url", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_fallback_method", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_laml_application_id", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "message_relay_context", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -27421,17 +35096,18 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" } } }, - "Recordings": { + "SipProfile": { "methods": { "__init__": { "params": [ @@ -27447,40 +35123,12 @@ ], "returns": "void" }, - "delete": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - } - ], - "returns": "any" - }, "get": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, { "name": "request_options", "kind": "keyword", @@ -27498,12 +35146,49 @@ ], "returns": "any" }, - "list": { + "update": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "domain_identifier", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_codecs", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_ciphers", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_encryption", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "default_send_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -27512,7 +35197,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -27523,7 +35208,7 @@ } } }, - "RegistryBrands": { + "VerifiedCallers": { "methods": { "__init__": { "params": [ @@ -27545,11 +35230,33 @@ "name": "self", "kind": "self" }, + { + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + }, { "name": "body", "kind": "positional", "type": "dict", "required": true + } + ], + "returns": "any" + }, + "delete": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true }, { "name": "request_options", @@ -27561,7 +35268,7 @@ ], "returns": "any" }, - "create_campaign": { + "redial_verification": { "params": [ { "name": "self", @@ -27574,22 +35281,58 @@ "required": true }, { - "name": "body", + "name": "request_options", + "kind": "keyword", + "type": "optional", + "required": false, + "default": null + } + ], + "returns": "any" + }, + "submit_verification": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "id", "kind": "positional", - "type": "dict", + "type": "string", + "required": true + }, + { + "name": "verification_code", + "kind": "keyword", + "type": "string", "required": true }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", "type": "optional", "required": false, "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "get": { + "update": { "params": [ { "name": "self", @@ -27609,21 +35352,48 @@ "default": null }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" + } + } + } + } + }, + "signalwire.rest.namespaces.video_resources_generated": { + "classes": { + "VideoConferenceTokens": { + "methods": { + "__init__": { + "params": [ + { + "name": "self", + "kind": "self" + }, + { + "name": "client", + "type": "class:signalwire.rest._base.HttpClient", + "required": true + } + ], + "returns": "void" }, - "list": { + "get": { "params": [ { "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -27641,7 +35411,7 @@ ], "returns": "any" }, - "list_campaigns": { + "reset": { "params": [ { "name": "self", @@ -27659,20 +35429,13 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" } } }, - "RegistryCampaigns": { + "VideoConferences": { "methods": { "__init__": { "params": [ @@ -27688,37 +35451,12 @@ ], "returns": "void" }, - "create_order": { + "create": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "phone_numbers", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "status_callback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -27727,16 +35465,15 @@ "default": null }, { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" }, - "get": { + "create_stream": { "params": [ { "name": "self", @@ -27748,6 +35485,19 @@ "type": "string", "required": true }, + { + "name": "url", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -27756,7 +35506,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -27765,7 +35515,7 @@ ], "returns": "any" }, - "list_numbers": { + "delete": { "params": [ { "name": "self", @@ -27783,18 +35533,11 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "list_orders": { + "list_conference_tokens": { "params": [ { "name": "self", @@ -27823,7 +35566,7 @@ ], "returns": "any" }, - "update": { + "list_streams": { "params": [ { "name": "self", @@ -27835,19 +35578,6 @@ "type": "string", "required": true }, - { - "name": "name", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -27856,7 +35586,7 @@ "default": null }, { - "name": "kwargs", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -27864,26 +35594,8 @@ } ], "returns": "any" - } - } - }, - "RegistryNumbers": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, - "delete": { + "update": { "params": [ { "name": "self", @@ -27901,13 +35613,19 @@ "type": "optional", "required": false, "default": null + }, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": true } ], "returns": "any" } } }, - "RegistryOrders": { + "VideoRoomRecordings": { "methods": { "__init__": { "params": [ @@ -27923,7 +35641,7 @@ ], "returns": "void" }, - "get": { + "delete": { "params": [ { "name": "self", @@ -27941,34 +35659,9 @@ "type": "optional", "required": false, "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" - } - } - }, - "ShortCodes": { - "methods": { - "__init__": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true - } - ], - "returns": "void" }, "get": { "params": [ @@ -28022,7 +35715,7 @@ ], "returns": "any" }, - "update": { + "list_events": { "params": [ { "name": "self", @@ -28034,61 +35727,6 @@ "type": "string", "required": true }, - { - "name": "name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "message_handler", - "kind": "keyword", - "type": "dict", - "required": true - }, - { - "name": "message_request_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_request_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_fallback_url", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_fallback_method", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_laml_application_id", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "message_relay_context", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -28097,7 +35735,7 @@ "default": null }, { - "name": "kwargs", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -28108,7 +35746,7 @@ } } }, - "SipProfile": { + "VideoRoomSessions": { "methods": { "__init__": { "params": [ @@ -28130,6 +35768,12 @@ "name": "self", "kind": "self" }, + { + "name": "id", + "kind": "positional", + "type": "string", + "required": true + }, { "name": "request_options", "kind": "keyword", @@ -28147,49 +35791,12 @@ ], "returns": "any" }, - "update": { + "list": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "domain_identifier", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "default_codecs", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "default_ciphers", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "default_encryption", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "default_send_as", - "kind": "keyword", - "type": "optional", - "required": false - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -28198,7 +35805,7 @@ "default": null }, { - "name": "kwargs", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -28206,30 +35813,18 @@ } ], "returns": "any" - } - } - }, - "VerifiedCallers": { - "methods": { - "__init__": { + }, + "list_events": { "params": [ { "name": "self", "kind": "self" }, { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", + "name": "id", + "kind": "positional", + "type": "string", "required": true - } - ], - "returns": "void" - }, - "create": { - "params": [ - { - "name": "self", - "kind": "self" }, { "name": "request_options", @@ -28239,15 +35834,16 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" }, - "delete": { + "list_members": { "params": [ { "name": "self", @@ -28265,33 +35861,18 @@ "type": "optional", "required": false, "default": null - } - ], - "returns": "any" - }, - "redial_verification": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true }, { - "name": "request_options", - "kind": "keyword", - "type": "optional", + "name": "params", + "kind": "var_keyword", + "type": "any", "required": false, - "default": null + "default": {} } ], "returns": "any" }, - "submit_verification": { + "list_recordings": { "params": [ { "name": "self", @@ -28303,19 +35884,6 @@ "type": "string", "required": true }, - { - "name": "verification_code", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, { "name": "request_options", "kind": "keyword", @@ -28324,7 +35892,7 @@ "default": null }, { - "name": "kwargs", + "name": "params", "kind": "var_keyword", "type": "any", "required": false, @@ -28333,18 +35901,12 @@ ], "returns": "any" }, - "update": { + "paginate": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, { "name": "request_options", "kind": "keyword", @@ -28353,21 +35915,18 @@ "default": null }, { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" } } - } - } - }, - "signalwire.rest.namespaces.video_resources_generated": { - "classes": { - "VideoConferenceTokens": { + }, + "VideoRoomTokens": { "methods": { "__init__": { "params": [ @@ -28383,46 +35942,126 @@ ], "returns": "void" }, - "get": { + "create": { "params": [ { "name": "self", "kind": "self" }, { - "name": "id", - "kind": "positional", + "name": "room_name", + "kind": "keyword", "type": "string", "required": true }, { - "name": "request_options", + "name": "user_name", "kind": "keyword", - "type": "optional", - "required": false, - "default": null + "type": "optional", + "required": false }, { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "reset": { - "params": [ + "name": "permissions", + "kind": "keyword", + "type": "optional", + "required": false + }, { - "name": "self", - "kind": "self" + "name": "join_from", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_until", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "remove_at", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "remove_after_seconds_elapsed", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_audio_muted", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_video_muted", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "auto_create_room", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "enable_room_previews", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "room_display_name", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "end_room_session_on_leave", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "join_as", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "media_allowed", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "room_meta", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "meta", + "kind": "keyword", + "type": "optional", + "required": false + }, + { + "name": "sync_audio_video", + "kind": "keyword", + "type": "optional", + "required": false }, { - "name": "id", - "kind": "positional", - "type": "string", - "required": true + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null }, { "name": "request_options", @@ -28430,13 +36069,20 @@ "type": "optional", "required": false, "default": null + }, + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": false, + "default": {} } ], "returns": "any" } } }, - "VideoConferences": { + "VideoRooms": { "methods": { "__init__": { "params": [ @@ -28538,35 +36184,6 @@ ], "returns": "any" }, - "list_conference_tokens": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, "list_streams": { "params": [ { @@ -28626,7 +36243,7 @@ } } }, - "VideoRoomRecordings": { + "VideoStreams": { "methods": { "__init__": { "params": [ @@ -28693,30 +36310,7 @@ ], "returns": "any" }, - "list": { - "params": [ - { - "name": "self", - "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} - } - ], - "returns": "any" - }, - "list_events": { + "update": { "params": [ { "name": "self", @@ -28728,6 +36322,19 @@ "type": "string", "required": true }, + { + "name": "url", + "kind": "keyword", + "type": "string", + "required": true + }, + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": false, + "default": null + }, { "name": "request_options", "kind": "keyword", @@ -28736,7 +36343,7 @@ "default": null }, { - "name": "params", + "name": "kwargs", "kind": "var_keyword", "type": "any", "required": false, @@ -28746,8 +36353,12 @@ "returns": "any" } } - }, - "VideoRoomSessions": { + } + } + }, + "signalwire.rest.namespaces.voice_resources_generated": { + "classes": { + "VoiceLogs": { "methods": { "__init__": { "params": [ @@ -28844,18 +36455,12 @@ ], "returns": "any" }, - "list_members": { + "paginate": { "params": [ { "name": "self", "kind": "self" }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, { "name": "request_options", "kind": "keyword", @@ -28872,617 +36477,520 @@ } ], "returns": "any" - }, - "list_recordings": { + } + } + } + } + }, + "signalwire.skills.api_ninjas_trivia.skill": { + "classes": { + "ApiNinjasTriviaSkill": { + "methods": {} + } + } + }, + "signalwire.skills.claude_skills.skill": { + "classes": { + "ClaudeSkillsSkill": { + "methods": { + "get_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" }, - "paginate": { + "register_tools": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" } } - }, - "VideoRoomTokens": { + } + } + }, + "signalwire.skills.datasphere.skill": { + "classes": { + "DataSphereSkill": { "methods": { - "__init__": { + "cleanup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "create": { + "get_global_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "room_name", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "user_name", - "kind": "keyword", - "type": "optional", - "required": false - }, + } + ], + "returns": "any" + }, + "get_hints": { + "params": [ { - "name": "permissions", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ { - "name": "join_from", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ { - "name": "join_until", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ { - "name": "remove_at", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "remove_after_seconds_elapsed", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.datasphere_serverless.skill": { + "classes": { + "DataSphereServerlessSkill": { + "methods": { + "get_global_data": { + "params": [ { - "name": "join_audio_muted", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_hints": { + "params": [ { - "name": "join_video_muted", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ { - "name": "auto_create_room", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ { - "name": "enable_room_previews", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ { - "name": "room_display_name", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "end_room_session_on_leave", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.datetime.skill": { + "classes": { + "DateTimeSkill": { + "methods": { + "get_hints": { + "params": [ { - "name": "join_as", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_parameter_schema": { + "params": [ { - "name": "media_allowed", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ { - "name": "room_meta", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "meta", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.google_maps.skill": { + "classes": { + "GoogleMapsSkill": { + "methods": { + "get_hints": { + "params": [ { - "name": "sync_audio_video", - "kind": "keyword", - "type": "optional", - "required": false - }, + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_parameter_schema": { + "params": [ { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" } } - }, - "VideoRooms": { + } + } + }, + "signalwire.skills.info_gatherer.skill": { + "classes": { + "InfoGathererSkill": { "methods": { - "__init__": { + "get_global_data": { "params": [ { "name": "self", "kind": "self" - }, + } + ], + "returns": "any" + }, + "get_instance_key": { + "params": [ { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ + { + "name": "self", + "kind": "self" } ], "returns": "void" }, - "create": { + "setup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.joke.skill": { + "classes": { + "JokeSkill": { + "methods": { + "get_global_data": { + "params": [ { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true + "name": "self", + "kind": "self" } ], "returns": "any" }, - "create_stream": { + "get_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "url", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "list" }, - "delete": { + "get_parameter_schema": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null } ], "returns": "any" }, - "list_streams": { + "register_tools": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "void" }, - "update": { + "setup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "body", - "kind": "positional", - "type": "dict", - "required": true } ], - "returns": "any" + "returns": "bool" } } - }, - "VideoStreams": { + } + } + }, + "signalwire.skills.math.skill": { + "classes": { + "MathSkill": { "methods": { - "__init__": { + "get_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], - "returns": "void" + "returns": "list" }, - "delete": { + "get_parameter_schema": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null } ], "returns": "any" }, - "get": { + "register_tools": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "void" }, - "update": { + "setup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "url", - "kind": "keyword", - "type": "string", - "required": true - }, - { - "name": "extras", - "kind": "keyword", - "type": "optional>", - "required": false, - "default": null - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "kwargs", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "bool" } } } } }, - "signalwire.rest.namespaces.voice_resources_generated": { + "signalwire.skills.native_vector_search.skill": { "classes": { - "VoiceLogs": { + "NativeVectorSearchSkill": { "methods": { - "__init__": { + "cleanup": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "client", - "type": "class:signalwire.rest._base.HttpClient", - "required": true } ], "returns": "void" }, - "get": { + "get_global_data": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], "returns": "any" }, - "list": { + "get_hints": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, - { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} } ], - "returns": "any" + "returns": "list" }, - "list_events": { + "get_instance_key": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "id", - "kind": "positional", - "type": "string", - "required": true - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], "returns": "any" }, - "paginate": { + "register_tools": { "params": [ { "name": "self", "kind": "self" - }, - { - "name": "request_options", - "kind": "keyword", - "type": "optional", - "required": false, - "default": null - }, + } + ], + "returns": "void" + }, + "setup": { + "params": [ { - "name": "params", - "kind": "var_keyword", - "type": "any", - "required": false, - "default": {} + "name": "self", + "kind": "self" } ], - "returns": "any" + "returns": "bool" } } } } }, + "signalwire.skills.play_background_file.skill": { + "classes": { + "PlayBackgroundFileSkill": { + "methods": {} + } + } + }, "signalwire.skills.registry": { "classes": { "SkillRegistry": { @@ -29624,6 +37132,152 @@ } } }, + "signalwire.skills.spider.skill": { + "classes": { + "SpiderSkill": { + "methods": { + "remove_xpaths": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + } + } + } + } + }, + "signalwire.skills.swml_transfer.skill": { + "classes": { + "SWMLTransferSkill": { + "methods": { + "get_hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.weather_api.skill": { + "classes": { + "WeatherApiSkill": { + "methods": {} + } + } + }, + "signalwire.skills.web_search.skill": { + "classes": { + "WebSearchSkill": { + "methods": { + "get_global_data": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "get_hints": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" + }, + "get_instance_key": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "get_parameter_schema": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "any" + }, + "register_tools": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "void" + }, + "setup": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + } + } + } + } + }, + "signalwire.skills.wikipedia_search.skill": { + "classes": { + "WikipediaSearchSkill": { + "methods": {} + } + } + }, "signalwire.swaig.parameter_schema": { "classes": { "ParameterSchema": { @@ -29657,7 +37311,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29677,7 +37331,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29711,7 +37365,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29731,7 +37385,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29751,7 +37405,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29776,7 +37430,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29828,6 +37482,15 @@ ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" }, + "size": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "int" + }, "string": { "params": [ { @@ -29843,7 +37506,7 @@ "name": "description", "type": "string", "required": false, - "default": null + "default": "" } ], "returns": "class:signalwire.swaig.parameter_schema.ParameterSchema" @@ -29987,7 +37650,7 @@ "name": "indent", "type": "int", "required": false, - "default": null + "default": -1 } ], "returns": "string" @@ -30179,13 +37842,13 @@ "name": "schema_path", "type": "string", "required": false, - "default": null + "default": "" }, { "name": "schema_validation", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "void" @@ -30406,7 +38069,7 @@ "name": "allow_private", "type": "bool", "required": false, - "default": null + "default": false } ], "returns": "bool" @@ -30427,7 +38090,7 @@ "name": "port", "type": "int", "required": false, - "default": null + "default": 8002 }, { "name": "directories", @@ -30451,7 +38114,7 @@ "name": "enable_directory_browsing", "type": "bool", "required": false, - "default": null + "default": false }, { "name": "allowed_extensions", @@ -30475,7 +38138,7 @@ "name": "enable_cors", "type": "bool", "required": false, - "default": null + "default": true } ], "returns": "void" @@ -30600,7 +38263,7 @@ "name": "host", "type": "string", "required": false, - "default": null + "default": "0.0.0.0" }, { "name": "bind_port", @@ -31154,6 +38817,30 @@ } } }, + "signalwire.core.auth_handler.BasicCredentials": { + "params": { + "password": { + "type": "string", + "required": false + }, + "username": { + "type": "string", + "required": false + } + } + }, + "signalwire.core.auth_handler.BearerCredentials": { + "params": { + "credentials": { + "type": "string", + "required": false + }, + "scheme": { + "type": "string", + "required": false + } + } + }, "signalwire.core.config_loader.ConfigLoader": { "params": { "config_paths": { @@ -31221,7 +38908,7 @@ "signalwire.core.contexts.GatherInfo": { "params": { "completion_action": { - "type": "string", + "type": "optional", "required": false }, "isolated": { @@ -31229,11 +38916,11 @@ "required": false }, "output_key": { - "type": "string", + "type": "optional", "required": false }, "prompt": { - "type": "string", + "type": "optional", "required": false } } @@ -31257,7 +38944,7 @@ "required": true }, "prompt": { - "type": "string", + "type": "optional", "required": false }, "question": { diff --git a/port_surface.json b/port_surface.json index 233acef..f8996f6 100644 --- a/port_surface.json +++ b/port_surface.json @@ -1,5 +1,5 @@ { - "generated_from": "signalwire-cpp @ 4875e642abb190b9dba8a9c30eadefdb605f61cd", + "generated_from": "signalwire-cpp @ 96c1e29639092cfcdd3cf066e160766faa2fc88b", "modules": { "signalwire": { "classes": {}, @@ -94,15 +94,18 @@ "AuthenticationError": [], "ChatInProgressError": [], "ChatLog": [ + "__init__", "call_timeline", "messages" ], "ChatResponse": [ + "__init__", "conversation_id", "text", "user_event" ], "ConversationInfo": [ + "__init__", "id", "initial_message", "status" @@ -286,6 +289,16 @@ "verify_api_key", "verify_basic_auth", "verify_bearer_token" + ], + "BasicCredentials": [ + "__init__", + "password", + "username" + ], + "BearerCredentials": [ + "__init__", + "credentials", + "scheme" ] }, "functions": [] @@ -346,7 +359,6 @@ "ContextBuilder": [ "__init__", "add_context", - "attach_tool_name_supplier", "get_context", "has_contexts", "reset", @@ -410,7 +422,6 @@ "classes": { "DataMap": [ "__init__", - "body", "description", "error_keys", "expression", @@ -656,7 +667,9 @@ "PostPromptEot": [], "PostPromptStampsUs": [], "PostPromptSwaigLogEntry": [ - "post_data" + "delayed_post_response", + "post_data", + "post_response" ], "PostPromptSystemEntry": [], "PostPromptSystemLogEntry": [], @@ -801,6 +814,8 @@ "ContextSwitchAction": [], "HoldAction": [], "PlaybackBgAction": [], + "SwaigAction": [], + "SwaigResponse": [], "TransferAction": [] }, "functions": [] @@ -912,6 +927,7 @@ "denoise", "detect_machine", "document", + "domain", "enter_queue", "execute", "extract_introspect_payload", @@ -967,6 +983,9 @@ "set_auth", "sip_refer", "sleep", + "ssl_cert_path", + "ssl_enabled", + "ssl_key_path", "stop", "stop_denoise", "stop_record_call", @@ -1507,6 +1526,7 @@ "Action": [ "__init__", "call", + "completed", "control_id", "is_done", "result", @@ -1564,7 +1584,6 @@ "play_silence", "play_tts", "project_id", - "prompt", "prompt_audio", "prompt_tts", "queue_enter", @@ -1731,6 +1750,7 @@ "signalwire.relay.event": { "classes": { "CallReceiveEvent": [ + "__init__", "call_state", "context", "device", @@ -1742,6 +1762,7 @@ "tag" ], "CallStateEvent": [ + "__init__", "call_state", "device", "direction", @@ -1749,11 +1770,13 @@ "from_payload" ], "CallingErrorEvent": [ + "__init__", "code", "from_payload", "message" ], "CollectEvent": [ + "__init__", "control_id", "final", "from_payload", @@ -1761,45 +1784,54 @@ "state" ], "ConferenceEvent": [ + "__init__", "conference_id", "from_payload", "name", "status" ], "ConnectEvent": [ + "__init__", "connect_state", "from_payload", "peer" ], "DenoiseEvent": [ + "__init__", "denoised", "from_payload" ], "DetectEvent": [ + "__init__", "control_id", "detect", "from_payload" ], "DialEvent": [ + "__init__", "call", "dial_state", "from_payload", "tag" ], "EchoEvent": [ + "__init__", "from_payload", "state" ], "FaxEvent": [ + "__init__", "control_id", "fax", "from_payload" ], "HoldEvent": [ + "__init__", "from_payload", "state" ], "MessageReceiveEvent": [ + "__init__", "body", "context", "direction", @@ -1813,6 +1845,7 @@ "to_number" ], "MessageStateEvent": [ + "__init__", "body", "context", "direction", @@ -1827,16 +1860,19 @@ "to_number" ], "PayEvent": [ + "__init__", "control_id", "from_payload", "state" ], "PlayEvent": [ + "__init__", "control_id", "from_payload", "state" ], "QueueEvent": [ + "__init__", "control_id", "from_payload", "position", @@ -1846,6 +1882,7 @@ "status" ], "RecordEvent": [ + "__init__", "control_id", "duration", "from_payload", @@ -1855,6 +1892,7 @@ "url" ], "ReferEvent": [ + "__init__", "from_payload", "sip_notify_response_code", "sip_refer_response_code", @@ -1862,6 +1900,7 @@ "state" ], "RelayEvent": [ + "__init__", "call_id", "event_type", "from_payload", @@ -1869,11 +1908,13 @@ "timestamp" ], "SendDigitsEvent": [ + "__init__", "control_id", "from_payload", "state" ], "StreamEvent": [ + "__init__", "control_id", "from_payload", "name", @@ -1881,6 +1922,7 @@ "url" ], "TapEvent": [ + "__init__", "control_id", "device", "from_payload", @@ -1888,6 +1930,7 @@ "tap" ], "TranscribeEvent": [ + "__init__", "control_id", "duration", "from_payload", @@ -2160,6 +2203,7 @@ "signalwire.rest._request_options": { "classes": { "RequestOptions": [ + "__init__", "abort_signal", "merge", "retries", @@ -3565,7 +3609,6 @@ "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3579,7 +3622,6 @@ "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3591,7 +3633,6 @@ "DateTimeSkill": [ "get_hints", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3603,7 +3644,6 @@ "GoogleMapsSkill": [ "get_hints", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3628,7 +3668,6 @@ "get_global_data", "get_hints", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3640,7 +3679,6 @@ "MathSkill": [ "get_hints", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3655,7 +3693,6 @@ "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3703,6 +3740,7 @@ "get_instance_key", "get_parameter_schema", "register_tools", + "remove_xpaths", "setup" ] }, @@ -3714,7 +3752,6 @@ "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3740,7 +3777,6 @@ "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup" ] @@ -3752,7 +3788,6 @@ "WikipediaSearchSkill": [ "get_hints", "get_parameter_schema", - "get_prompt_sections", "register_tools", "search_wiki", "setup" diff --git a/port_surface_native.json b/port_surface_native.json index f84ea37..b513c85 100644 --- a/port_surface_native.json +++ b/port_surface_native.json @@ -799,9 +799,11 @@ "SummarizeAction", "SummarizeConversationSWAIGFunction", "SurveyAgent", + "SwaigAction", "SwaigArgument", "SwaigRequest", "SwaigRequestData", + "SwaigResponse", "Switch", "SwitchConfig", "SwmlRenderer", @@ -957,6 +959,7 @@ "ai_message", "ai_model", "ai_name", + "ai_params", "ai_session_id", "ai_sidecar", "ai_start_date", @@ -988,7 +991,6 @@ "assign_domain_application", "assign_phone_route", "async", - "attach_tool_name_supplier", "attempted", "attempts", "attention_timeout", @@ -1008,6 +1010,7 @@ "auto_map_sip_usernames", "average_wait_time", "avg_tps", + "back_to_back_functions", "background_file", "background_file_loops", "background_file_volume", @@ -1027,6 +1030,7 @@ "billing_ms", "bind_digit", "bind_method", + "bind_params", "blocked_extensions", "body", "boolean", @@ -1268,6 +1272,7 @@ "dial", "dial_state", "dial_state_enum", + "dial_timeout", "dialogflow_agent", "dialogflow_reference_id", "dialogflow_reference_name", @@ -1373,6 +1378,7 @@ "expression", "expressions", "extension", + "extensive_data", "external_paths", "extra_swaig_fields", "extract_introspect_payload", @@ -1434,6 +1440,7 @@ "function_wait_for_talking", "functions", "functions_on_no_response", + "functions_on_speaker_timeout", "gather_info", "generate_method_body", "generate_method_signature", @@ -2092,6 +2099,7 @@ "set_departments", "set_device", "set_direction", + "set_domain", "set_dynamic_config_callback", "set_end", "set_end_of_speech_timeout", @@ -2165,6 +2173,9 @@ "set_skip_user_turn", "set_special_instructions", "set_speech_event_timeout", + "set_ssl_cert_path", + "set_ssl_enabled", + "set_ssl_key_path", "set_state", "set_static_dir", "set_step_criteria", @@ -2186,6 +2197,7 @@ "set_voice", "set_web_hook_url", "set_webhook_url", + "settings", "setup", "setup_graceful_shutdown", "setup_sip_routing", @@ -2271,9 +2283,7 @@ "status_pushed_wall_us", "status_url", "status_url_method", - "step", "step_criteria", - "step_index", "step_order", "steps", "stereo", diff --git a/relay/examples/relay_answer_and_welcome.cpp b/relay/examples/relay_answer_and_welcome.cpp index a8fc659..252aa96 100644 --- a/relay/examples/relay_answer_and_welcome.cpp +++ b/relay/examples/relay_answer_and_welcome.cpp @@ -2,32 +2,37 @@ // RELAY: Answer an inbound call and play a TTS greeting. // NOTE: Transport is stubbed; demonstrates the API surface. -#include #include +#include using namespace signalwire::relay; int main() { - auto client = RelayClient::from_env(); - - client.on_call([](Call& call) { - std::cout << "Inbound call from " << call.from() << "\n"; - - // Answer the call - call.answer(); - - // Play TTS greeting - auto action = call.play({ - {{"type", "tts"}, {"params", {{"text", "Welcome to SignalWire! How can I help you today?"}}}} - }); - action.wait(); - - // Hang up - call.hangup(); - call.wait_for_ended(10000); - std::cout << "Call ended\n"; - }); - - std::cout << "Waiting for inbound calls...\n"; - client.run(); + auto client = RelayClient::from_env(); + + client.on_call([](Call& call) { + std::cout << "Inbound call from " << call.from() << "\n"; + + // Answer the call + call.answer(); + + // Play TTS greeting. wait() returns false if the action timed out instead of + // completing — check it rather than assuming the greeting was heard. + auto action = + call.play({{{"type", "tts"}, + {"params", {{"text", "Welcome to SignalWire! How can I help you today?"}}}}}); + if (!action.wait()) { + std::cerr << "Greeting playback timed out\n"; + } + + // Hang up + call.hangup(); + if (!call.wait_for_ended(10000)) { + std::cerr << "Call did not reach the ended state within 10s\n"; + } + std::cout << "Call ended\n"; + }); + + std::cout << "Waiting for inbound calls...\n"; + client.run(); } diff --git a/relay/examples/relay_dial_and_play.cpp b/relay/examples/relay_dial_and_play.cpp index 520893f..71c871e 100644 --- a/relay/examples/relay_dial_and_play.cpp +++ b/relay/examples/relay_dial_and_play.cpp @@ -2,19 +2,22 @@ // RELAY: Dial an outbound call and play TTS. // NOTE: Transport is stubbed; demonstrates the API surface. -#include #include #include +#include using namespace signalwire::relay; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { const char* from_env = std::getenv("RELAY_FROM_NUMBER"); const char* to_env = std::getenv("RELAY_TO_NUMBER"); if (!from_env || !to_env) { - std::cerr << "Set RELAY_FROM_NUMBER and RELAY_TO_NUMBER\n"; - return 1; + std::cerr << "Set RELAY_FROM_NUMBER and RELAY_TO_NUMBER\n"; + return 1; } std::string from_number = from_env; std::string to_number = to_env; @@ -24,22 +27,29 @@ int main() { std::cout << "Connected\n"; // Dial - json devices = {{ - {{"type", "phone"}, {"params", {{"to_number", to_number}, {"from_number", from_number}}}} - }}; + json devices = {{{{"type", "phone"}, + {"params", {{"to_number", to_number}, {"from_number", from_number}}}}}}; Call call = client.dial(devices); std::cout << "Dialing " << to_number << " — call_id: " << call.call_id() << "\n"; - // Play TTS - auto action = call.play({ - {{"type", "tts"}, {"params", {{"text", "Hello from SignalWire!"}}}} - }); - action.wait(); - std::cout << "Playback finished\n"; + // Play TTS. wait() returns false on timeout rather than completion, so check + // it — otherwise "Playback finished" prints even when it did not. + auto action = call.play({{{"type", "tts"}, {"params", {{"text", "Hello from SignalWire!"}}}}}); + if (action.wait()) { + std::cout << "Playback finished\n"; + } else { + std::cerr << "Playback timed out\n"; + } call.hangup(); - call.wait_for_ended(); + if (!call.wait_for_ended()) { + std::cerr << "Call did not reach the ended state before the timeout\n"; + } std::cout << "Call ended\n"; client.disconnect(); + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/relay/examples/relay_ivr_connect.cpp b/relay/examples/relay_ivr_connect.cpp index 62e8b18..db9f8ee 100644 --- a/relay/examples/relay_ivr_connect.cpp +++ b/relay/examples/relay_ivr_connect.cpp @@ -2,46 +2,55 @@ // RELAY: IVR with DTMF collection and call connect. // NOTE: Transport is stubbed; demonstrates the API surface. -#include #include +#include using namespace signalwire::relay; using json = nlohmann::json; int main() { - auto client = RelayClient::from_env(); - - client.on_call([](Call& call) { - std::cout << "Inbound call: " << call.call_id() << "\n"; - call.answer(); - - // Play IVR menu - auto menu = call.play({ - {{"type", "tts"}, {"params", {{"text", - "Press 1 for sales, 2 for support, or 3 for billing."}}}} - }); - menu.wait(); - - // Collect DTMF - auto collect = call.collect({ - {"digits", {{"max", 1}, {"terminators", "#"}}}, - {"speech", {{"hints", json::array({"sales", "support", "billing"})}}}, - {"initial_timeout", 5.0}, - {"partial_results", true} - }); - collect.wait(); - - // Route based on input (stub: always route to sales) - std::cout << "Routing call...\n"; - auto connect_action = call.connect({{ - {{"type", "phone"}, {"params", {{"to_number", "+15551001"}}}} - }}); - connect_action.wait(); - - call.hangup(); - std::cout << "Call ended\n"; - }); - - std::cout << "IVR demo waiting for calls...\n"; - client.run(); + auto client = RelayClient::from_env(); + + client.on_call([](Call& call) { + std::cout << "Inbound call: " << call.call_id() << "\n"; + call.answer(); + + // Play IVR menu. wait() returns false if the action timed out rather than + // completing — always check it, or you will act on a menu the caller never + // finished hearing. + auto menu = call.play( + {{{"type", "tts"}, + {"params", {{"text", "Press 1 for sales, 2 for support, or 3 for billing."}}}}}); + if (!menu.wait()) { + std::cerr << "Menu playback did not complete; hanging up\n"; + call.hangup(); + return; + } + + // Collect DTMF + auto collect = + call.collect({{"digits", {{"max", 1}, {"terminators", "#"}}}, + {"speech", {{"hints", json::array({"sales", "support", "billing"})}}}, + {"initial_timeout", 5.0}, + {"partial_results", true}}); + if (!collect.wait()) { + std::cerr << "No input collected; hanging up\n"; + call.hangup(); + return; + } + + // Route based on input (stub: always route to sales) + std::cout << "Routing call...\n"; + auto connect_action = + call.connect({{{{"type", "phone"}, {"params", {{"to_number", "+15551001"}}}}}}); + if (!connect_action.wait()) { + std::cerr << "Connect did not complete\n"; + } + + call.hangup(); + std::cout << "Call ended\n"; + }); + + std::cout << "IVR demo waiting for calls...\n"; + client.run(); } diff --git a/rest/examples/rest_10dlc_registration.cpp b/rest/examples/rest_10dlc_registration.cpp index 9783bdb..36ea98c 100644 --- a/rest/examples/rest_10dlc_registration.cpp +++ b/rest/examples/rest_10dlc_registration.cpp @@ -1,29 +1,36 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: 10DLC registration workflow. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Check verified callers - auto callers = client.verified_callers().list(); - std::cout << "Verified callers: " << callers.dump(2) << "\n"; + // Check verified callers + auto callers = client.verified_callers().list(); + std::cout << "Verified callers: " << callers.dump(2) << "\n"; - // Registry entries (10DLC brand registrations) - auto registry = client.registry().brands.list(); - std::cout << "Registry brands: " << registry.dump(2) << "\n"; + // Registry entries (10DLC brand registrations) + auto registry = client.registry().brands.list(); + std::cout << "Registry brands: " << registry.dump(2) << "\n"; - // Number groups - auto groups = client.number_groups().list(); - std::cout << "Number groups: " << groups.dump(2) << "\n"; + // Number groups + auto groups = client.number_groups().list(); + std::cout << "Number groups: " << groups.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_bind_phone_to_swml_webhook.cpp b/rest/examples/rest_bind_phone_to_swml_webhook.cpp index 383f5bc..9940d66 100644 --- a/rest/examples/rest_bind_phone_to_swml_webhook.cpp +++ b/rest/examples/rest_bind_phone_to_swml_webhook.cpp @@ -14,59 +14,68 @@ // PHONE_NUMBER_SID - SID of a phone number you own (pn-...) // SWML_WEBHOOK_URL - your backend's SWML endpoint -#include -#include #include #include +#include +#include #include using namespace signalwire::rest; using json = nlohmann::json; static std::string env_or_die(const char* key) { - const char* v = std::getenv(key); - if (!v || !*v) { - std::cerr << "Missing required env var: " << key << "\n"; - std::exit(1); - } - return v; + const char* v = std::getenv(key); + if (!v || !*v) { + std::cerr << "Missing required env var: " << key << "\n"; + std::exit(1); + } + return v; } int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - const std::string pn_sid = env_or_die("PHONE_NUMBER_SID"); - const std::string webhook_url = env_or_die("SWML_WEBHOOK_URL"); + const std::string pn_sid = env_or_die("PHONE_NUMBER_SID"); + const std::string webhook_url = env_or_die("SWML_WEBHOOK_URL"); - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // The typed helper — one line: - std::cout << "Binding " << pn_sid << " to " << webhook_url << " ...\n"; - client.phone_numbers().set_swml_webhook(pn_sid, {.url = webhook_url}); + // The typed helper — one line. It returns the updated phone-number record; + // keep it so you can confirm the binding actually took. + std::cout << "Binding " << pn_sid << " to " << webhook_url << " ...\n"; + auto bound = client.phone_numbers().set_swml_webhook(pn_sid, {.url = webhook_url}); + std::cout << "Bound: " << bound.dump() << "\n"; - // The equivalent wire-level form (use this if you need unusual fields): - // - // client.phone_numbers().update(pn_sid, { - // {"call_handler", to_wire_string(PhoneCallHandler::RelayScript)}, - // {"call_relay_script_url", webhook_url}, - // }); + // The equivalent wire-level form (use this if you need unusual fields): + // + // client.phone_numbers().update(pn_sid, { + // {"call_handler", to_wire_string(PhoneCallHandler::RelayScript)}, + // {"call_relay_script_url", webhook_url}, + // }); - // Verify: the server auto-created a swml_webhook Fabric resource. - auto pn = client.phone_numbers().get(pn_sid); - std::cout << " call_handler = " << pn.value("call_handler", "") << "\n"; - std::cout << " call_relay_script_url = " << pn.value("call_relay_script_url", "") << "\n"; - std::cout << " calling_handler_resource_id (server-derived) = " - << pn.value("calling_handler_resource_id", "") << "\n"; + // Verify: the server auto-created a swml_webhook Fabric resource. + auto pn = client.phone_numbers().get(pn_sid); + std::cout << " call_handler = " << pn.value("call_handler", "") << "\n"; + std::cout << " call_relay_script_url = " << pn.value("call_relay_script_url", "") << "\n"; + std::cout << " calling_handler_resource_id (server-derived) = " + << pn.value("calling_handler_resource_id", "") << "\n"; - // To route to something other than an SWML webhook, use: - // client.phone_numbers().set_cxml_webhook(sid, {.url = url}) // LAML / Twilio-compat - // client.phone_numbers().set_ai_agent(sid, {.agent_id = agent_id}) // AI Agent - // client.phone_numbers().set_call_flow(sid, {.flow_id = flow_id}) // Call Flow - // client.phone_numbers().set_relay_application(sid, {.name = name}) // Named RELAY app - // client.phone_numbers().set_relay_topic(sid, {.topic = topic}) // RELAY topic + // To route to something other than an SWML webhook, use: + // client.phone_numbers().set_cxml_webhook(sid, {.url = url}) // LAML / + // Twilio-compat client.phone_numbers().set_ai_agent(sid, {.agent_id = agent_id}) // AI + // Agent client.phone_numbers().set_call_flow(sid, {.flow_id = flow_id}) // Call Flow + // client.phone_numbers().set_relay_application(sid, {.name = name}) // Named RELAY app + // client.phone_numbers().set_relay_topic(sid, {.topic = topic}) // RELAY topic } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; - return 1; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + return 1; } return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_calling_ivr_and_ai.cpp b/rest/examples/rest_calling_ivr_and_ai.cpp index 956e1e4..e3e3ece 100644 --- a/rest/examples/rest_calling_ivr_and_ai.cpp +++ b/rest/examples/rest_calling_ivr_and_ai.cpp @@ -1,38 +1,46 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: IVR with collect and AI integration. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - auto call = client.calling().dial({ - .from = "+15559876543", - .to = "+15551234567", - .url = "https://example.com/handler", - }); - std::string call_id = call.value("call_id", ""); + auto call = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", + .url = "https://example.com/handler", + }); + std::string call_id = call.value("call_id", ""); - // Collect DTMF - auto collected = client.calling().collect(call_id, { - .initial_timeout = 10, - .digits = json{{"max", 1}, {"terminators", "#"}}, - }); - std::cout << "Collected: " << collected.dump() << "\n"; + // Collect DTMF + auto collected = + client.calling().collect(call_id, { + .initial_timeout = 10, + .digits = json{{"max", 1}, {"terminators", "#"}}, + }); + std::cout << "Collected: " << collected.dump() << "\n"; - // Detect answering machine - auto detect = client.calling().detect(call_id, { - .detect = {{"type", "machine"}}, - .timeout = 30, - }); - std::cout << "Detection: " << detect.dump() << "\n"; + // Detect answering machine + auto detect = client.calling().detect(call_id, { + .detect = {{"type", "machine"}}, + .timeout = 30, + }); + std::cout << "Detection: " << detect.dump() << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_calling_play_and_record.cpp b/rest/examples/rest_calling_play_and_record.cpp index 4c61eb0..761dff6 100644 --- a/rest/examples/rest_calling_play_and_record.cpp +++ b/rest/examples/rest_calling_play_and_record.cpp @@ -1,39 +1,51 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Place a call, play audio, and record. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); - - // Dial - auto call = client.calling().dial({ - .from = "+15559876543", - .to = "+15551234567", - .url = "https://example.com/handler", - }); - std::string call_id = call.value("call_id", ""); - std::cout << "Call ID: " << call_id << "\n"; - - // Play audio - client.calling().play(call_id, { - .play = json::array({{{"type", "tts"}, - {"params", {{"text", "Recording will begin now."}}}}}), - }); - - // Start recording - client.calling().record(call_id, { - .extras = {{"record", {{"stereo", true}, {"format", "wav"}}}}, - }); - - std::cout << "Playing and recording on call " << call_id << "\n"; + auto client = RestClient::from_env(); + + // Dial + auto call = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", + .url = "https://example.com/handler", + }); + std::string call_id = call.value("call_id", ""); + std::cout << "Call ID: " << call_id << "\n"; + + // Play audio. Each call returns the API's response body — keep it; that is + // where the control id you need to stop/inspect the action comes back. + auto play = client.calling().play( + call_id, { + .play = json::array({{{"type", "tts"}, + {"params", {{"text", "Recording will begin now."}}}}}), + }); + std::cout << " Play: " << play.dump() << "\n"; + + // Start recording + auto recording = client.calling().record( + call_id, { + .extras = {{"record", {{"stereo", true}, {"format", "wav"}}}}, + }); + std::cout << " Recording: " << recording.dump() << "\n"; + + std::cout << "Playing and recording on call " << call_id << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_datasphere_search.cpp b/rest/examples/rest_datasphere_search.cpp index 8e64395..b5c2121 100644 --- a/rest/examples/rest_datasphere_search.cpp +++ b/rest/examples/rest_datasphere_search.cpp @@ -1,33 +1,39 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Upload a document and run a semantic search via Datasphere. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a document - std::cout << "Creating document...\n"; - auto doc = client.datasphere().documents.create({ - {"name", "product-docs"}, - {"content", "SignalWire AI Agents SDK enables building voice AI applications."} - }); - std::cout << " Document: " << doc.dump(2) << "\n"; + // Create a document + std::cout << "Creating document...\n"; + auto doc = client.datasphere().documents.create( + {{"name", "product-docs"}, + {"content", "SignalWire AI Agents SDK enables building voice AI applications."}}); + std::cout << " Document: " << doc.dump(2) << "\n"; - // Search - std::cout << "\nSearching...\n"; - auto results = client.datasphere().documents.search({ - .query_string = "How to build AI agents?", - .count = 5, - }); - std::cout << " Results: " << results.dump(2) << "\n"; + // Search + std::cout << "\nSearching...\n"; + auto results = client.datasphere().documents.search({ + .query_string = "How to build AI agents?", + .count = 5, + }); + std::cout << " Results: " << results.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_fabric_conference_rooms.cpp b/rest/examples/rest_fabric_conference_rooms.cpp index 7bc9016..43dc7fe 100644 --- a/rest/examples/rest_fabric_conference_rooms.cpp +++ b/rest/examples/rest_fabric_conference_rooms.cpp @@ -1,35 +1,40 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Manage Fabric conference rooms. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a conference room - auto conf = client.fabric().conference_rooms.create({ - {"name", "team-standup"}, - {"max_members", 10} - }); - std::cout << "Conference room: " << conf.dump(2) << "\n"; - std::string room_id = conf.value("id", ""); + // Create a conference room + auto conf = + client.fabric().conference_rooms.create({{"name", "team-standup"}, {"max_members", 10}}); + std::cout << "Conference room: " << conf.dump(2) << "\n"; + std::string room_id = conf.value("id", ""); - // List conference rooms - auto rooms = client.fabric().conference_rooms.list(); - std::cout << "All rooms: " << rooms.dump(2) << "\n"; + // List conference rooms + auto rooms = client.fabric().conference_rooms.list(); + std::cout << "All rooms: " << rooms.dump(2) << "\n"; - // List the addresses attached to the room - if (!room_id.empty()) { - auto addresses = client.fabric().conference_rooms.list_addresses(room_id); - std::cout << "Room addresses: " << addresses.dump(2) << "\n"; - } + // List the addresses attached to the room + if (!room_id.empty()) { + auto addresses = client.fabric().conference_rooms.list_addresses(room_id); + std::cout << "Room addresses: " << addresses.dump(2) << "\n"; + } } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_fabric_subscribers_and_sip.cpp b/rest/examples/rest_fabric_subscribers_and_sip.cpp index 0186c1a..bc365e5 100644 --- a/rest/examples/rest_fabric_subscribers_and_sip.cpp +++ b/rest/examples/rest_fabric_subscribers_and_sip.cpp @@ -1,35 +1,38 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Manage Fabric subscribers and SIP endpoints. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a subscriber - auto sub = client.fabric().subscribers.create({ - {"first_name", "John"}, {"last_name", "Doe"}, - {"email", "john@example.com"} - }); - std::cout << "Subscriber: " << sub.dump(2) << "\n"; + // Create a subscriber + auto sub = client.fabric().subscribers.create( + {{"first_name", "John"}, {"last_name", "Doe"}, {"email", "john@example.com"}}); + std::cout << "Subscriber: " << sub.dump(2) << "\n"; - // Create a SIP endpoint - auto sip = client.fabric().sip_endpoints.create({ - {"name", "office-phone"}, {"username", "john"}, - {"password", "secure123"} - }); - std::cout << "SIP endpoint: " << sip.dump(2) << "\n"; + // Create a SIP endpoint + auto sip = client.fabric().sip_endpoints.create( + {{"name", "office-phone"}, {"username", "john"}, {"password", "secure123"}}); + std::cout << "SIP endpoint: " << sip.dump(2) << "\n"; - // List endpoints - auto endpoints = client.fabric().sip_endpoints.list(); - std::cout << "All SIP endpoints: " << endpoints.dump(2) << "\n"; + // List endpoints + auto endpoints = client.fabric().sip_endpoints.list(); + std::cout << "All SIP endpoints: " << endpoints.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_fabric_swml_and_callflows.cpp b/rest/examples/rest_fabric_swml_and_callflows.cpp index b8600e2..86cb380 100644 --- a/rest/examples/rest_fabric_swml_and_callflows.cpp +++ b/rest/examples/rest_fabric_swml_and_callflows.cpp @@ -1,40 +1,41 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Manage SWML scripts and call flows via Fabric API. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a SWML script - auto script = client.fabric().swml_scripts.create({ - {"name", "greeting-script"}, - {"content", { - {"version", "1.0.0"}, - {"sections", {{"main", json::array({ - {{"answer", json::object()}}, - {{"play", {{"url", "https://example.com/greeting.mp3"}}}}, - {{"hangup", json::object()}} - })}}} - }} - }); - std::cout << "SWML script: " << script.dump(2) << "\n"; + // Create a SWML script + auto script = client.fabric().swml_scripts.create( + {{"name", "greeting-script"}, + {"content", + {{"version", "1.0.0"}, + {"sections", + {{"main", json::array({{{"answer", json::object()}}, + {{"play", {{"url", "https://example.com/greeting.mp3"}}}}, + {{"hangup", json::object()}}})}}}}}}); + std::cout << "SWML script: " << script.dump(2) << "\n"; - // Create a call flow - auto flow = client.fabric().call_flows.create({ - {"name", "main-flow"}, - {"steps", json::array({ - {{"type", "ai"}, {"prompt", "You are a helpful assistant."}} - })} - }); - std::cout << "Call flow: " << flow.dump(2) << "\n"; + // Create a call flow + auto flow = client.fabric().call_flows.create( + {{"name", "main-flow"}, + {"steps", json::array({{{"type", "ai"}, {"prompt", "You are a helpful assistant."}}})}}); + std::cout << "Call flow: " << flow.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_manage_resources.cpp b/rest/examples/rest_manage_resources.cpp index cdda211..6ec397b 100644 --- a/rest/examples/rest_manage_resources.cpp +++ b/rest/examples/rest_manage_resources.cpp @@ -1,46 +1,53 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Create an AI agent, assign a phone number, place a test call. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); - - // Create an AI agent - std::cout << "Creating AI agent...\n"; - auto agent = client.fabric().ai_agents.create({ - {"name", "Demo Support Bot"}, - {"prompt", {{"text", "You are a friendly support agent."}}} - }); - std::string agent_id = agent.value("id", ""); - std::cout << " Created: " << agent_id << "\n"; - - // List agents - auto agents = client.fabric().ai_agents.list(); - std::cout << " Total agents: " << agents.dump() << "\n"; - - // Search phone numbers - auto numbers = client.phone_numbers().search({{"areacode", "512"}, {"max_results", "3"}}); - std::cout << " Available numbers: " << numbers.dump() << "\n"; - - // Place a test call - auto call = client.calling().dial({ - .from = "+15559876543", - .to = "+15551234567", - .url = "https://example.com/handler", - }); - std::cout << " Call: " << call.dump() << "\n"; - - // Cleanup - client.fabric().ai_agents.delete_(agent_id); - std::cout << " Deleted agent\n"; + auto client = RestClient::from_env(); + + // Create an AI agent + std::cout << "Creating AI agent...\n"; + auto agent = client.fabric().ai_agents.create( + {{"name", "Demo Support Bot"}, + {"prompt", {{"text", "You are a friendly support agent."}}}}); + std::string agent_id = agent.value("id", ""); + std::cout << " Created: " << agent_id << "\n"; + + // List agents + auto agents = client.fabric().ai_agents.list(); + std::cout << " Total agents: " << agents.dump() << "\n"; + + // Search phone numbers + auto numbers = client.phone_numbers().search({{"areacode", "512"}, {"max_results", "3"}}); + std::cout << " Available numbers: " << numbers.dump() << "\n"; + + // Place a test call + auto call = client.calling().dial({ + .from = "+15559876543", + .to = "+15551234567", + .url = "https://example.com/handler", + }); + std::cout << " Call: " << call.dump() << "\n"; + + // Cleanup. delete_ returns the API's response body; keep it rather than + // announcing a deletion you never looked at. + auto deleted = client.fabric().ai_agents.delete_(agent_id); + std::cout << " Deleted agent: " << deleted.dump() << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_phone_number_management.cpp b/rest/examples/rest_phone_number_management.cpp index 2e279a7..8f61799 100644 --- a/rest/examples/rest_phone_number_management.cpp +++ b/rest/examples/rest_phone_number_management.cpp @@ -1,28 +1,33 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Search, purchase, and manage phone numbers. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Search for numbers - std::cout << "Searching numbers in area code 512...\n"; - auto available = client.phone_numbers().search({ - {"areacode", "512"}, {"max_results", "5"} - }); - std::cout << "Available: " << available.dump(2) << "\n"; + // Search for numbers + std::cout << "Searching numbers in area code 512...\n"; + auto available = client.phone_numbers().search({{"areacode", "512"}, {"max_results", "5"}}); + std::cout << "Available: " << available.dump(2) << "\n"; - // List owned numbers - auto owned = client.phone_numbers().list(); - std::cout << "\nOwned numbers: " << owned.dump(2) << "\n"; + // List owned numbers + auto owned = client.phone_numbers().list(); + std::cout << "\nOwned numbers: " << owned.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_queues_mfa_and_recordings.cpp b/rest/examples/rest_queues_mfa_and_recordings.cpp index 73041b5..7970258 100644 --- a/rest/examples/rest_queues_mfa_and_recordings.cpp +++ b/rest/examples/rest_queues_mfa_and_recordings.cpp @@ -1,35 +1,40 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Queues, MFA, and recording management. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a queue - auto queue = client.queues().create({ - {"name", "support-queue"}, {"max_size", 50} - }); - std::cout << "Queue: " << queue.dump(2) << "\n"; + // Create a queue + auto queue = client.queues().create({{"name", "support-queue"}, {"max_size", 50}}); + std::cout << "Queue: " << queue.dump(2) << "\n"; - // List recordings - auto recordings = client.recordings().list(); - std::cout << "Recordings: " << recordings.dump(2) << "\n"; + // List recordings + auto recordings = client.recordings().list(); + std::cout << "Recordings: " << recordings.dump(2) << "\n"; - // MFA: send a verification code via SMS - auto mfa = client.mfa().sms({ - .to = "+15551234567", - .from = "+15559876543", - .message = "Your code is {code}", - }); - std::cout << "MFA request: " << mfa.dump(2) << "\n"; + // MFA: send a verification code via SMS + auto mfa = client.mfa().sms({ + .to = "+15551234567", + .from = "+15559876543", + .message = "Your code is {code}", + }); + std::cout << "MFA request: " << mfa.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/rest/examples/rest_video_rooms.cpp b/rest/examples/rest_video_rooms.cpp index 73351e9..8aef8be 100644 --- a/rest/examples/rest_video_rooms.cpp +++ b/rest/examples/rest_video_rooms.cpp @@ -1,33 +1,37 @@ // Copyright (c) 2025 SignalWire — MIT License // REST: Manage video rooms, sessions, and recordings. -#include #include +#include using namespace signalwire::rest; using json = nlohmann::json; int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { try { - auto client = RestClient::from_env(); + auto client = RestClient::from_env(); - // Create a video room - auto room = client.video().rooms.create({ - {"name", "team-meeting"}, - {"max_members", 10}, - {"quality", "1080p"} - }); - std::cout << "Room: " << room.dump(2) << "\n"; + // Create a video room + auto room = client.video().rooms.create( + {{"name", "team-meeting"}, {"max_members", 10}, {"quality", "1080p"}}); + std::cout << "Room: " << room.dump(2) << "\n"; - // List rooms - auto rooms = client.video().rooms.list(); - std::cout << "All rooms: " << rooms.dump(2) << "\n"; + // List rooms + auto rooms = client.video().rooms.list(); + std::cout << "All rooms: " << rooms.dump(2) << "\n"; - // List recordings - auto recordings = client.video().room_recordings.list(); - std::cout << "Recordings: " << recordings.dump(2) << "\n"; + // List recordings + auto recordings = client.video().room_recordings.list(); + std::cout << "Recordings: " << recordings.dump(2) << "\n"; } catch (const SignalWireRestError& e) { - std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; + std::cerr << "Error " << e.status_code() << ": " << e.what() << "\n"; } + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..3bdee07 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,94 @@ +# ruff config for signalwire-cpp — the REPO-LINT / REPO-FMT gates. +# +# This is a C++ SDK, but it carries 9 hand-written Python files under scripts/ +# (~10.4k lines) and until 2026-07-30 NOT ONE of them was linted or formatted by +# any gate. Two of them are load-bearing lint/format infrastructure themselves: +# +# scripts/_cpp_fmt.py the REST/type generators shell to it to run +# clang-format over emitted C++ +# scripts/clang_tidy_cache.py the ctcache wrapper the LINT gate itself routes +# every clang-tidy invocation through +# +# i.e. the tooling that enforces the bar on the C++ tree was itself below any +# bar. Linting it found a live defect on the first pass (a duplicate dict key in +# enumerate_surface.py that made a projection stanza dead code — fe9fd56). +# +# The rule selection MIRRORS the reference implementation's +# (signalwire-python/pyproject.toml) so the fleet is consistent. Do not invent a +# different set here; if the reference's set changes, follow it. + +target-version = "py310" +line-length = 88 + +# VENDORED THIRD-PARTY CODE — the one and only exclusion, and it is the same +# category as deps/ on the C++ side: code we do not own and must not edit. +# +# scripts/clang_tidy_cache.py is vendored verbatim from matus-chochlik/ctcache at +# a pinned upstream SHA (see its header). Editing it to satisfy a linter would +# fork it from upstream and break the documented re-vendor path ("to upgrade: +# re-vendor from a newer pinned SHA and re-audit"). Its bare `except: pass` +# blocks are in the OPTIONAL REST-server cache backend, and that fail-open +# behaviour is deliberate and audited: any cache error falls through to running +# the real clang-tidy, so a finding is never skipped (scripts/run-lint.sh:84-88). +# cpp uses local-filesystem mode only; the network paths are never taken. +# +# The other 8 files under scripts/ are first-party and fully linted. +# +# Written as a BASENAME glob rather than the relative path +# "scripts/clang_tidy_cache.py": ruff anchors a relative exclude to the config's +# directory, so the relative form silently stopped matching once the target was +# given as an ABSOLUTE path. Measured — 139 findings from this vendored file +# reappeared, turning a clean run into 148. A basename glob matches however the +# path is spelled. +exclude = ["**/clang_tidy_cache.py"] + +[format] +# Pin STABLE formatting explicitly. Without this, `ruff format --check` can +# resolve preview-mode formatting while a local stable run leaves the file +# unchanged — a local≠CI split with no visible cause. Same rationale, and the +# same value, as the reference's [tool.ruff.format]. +preview = false + +[lint] +# E4/E7/E9 + F are the ruff defaults (E501 line-length intentionally excluded — +# formatting is owned by `ruff format`, not the linter). B = flake8-bugbear +# (likely-bug patterns), S = flake8-bandit (security smells). Identical to the +# reference's select list. +select = ["E4", "E7", "E9", "F", "B", "S", "C4", "PERF", "SIM", "PTH", "RET", "RUF", "UP"] + +[lint.per-file-ignores] +# Keys are BASENAME globs, not "scripts/.py". A relative key is anchored +# to the config directory and silently stops matching when the target is passed +# as an absolute path -- the same failure the `exclude` above hit, and the same +# one that silently changed 7 findings' status in a sibling port. These file +# names are unique in this repo, so a basename glob is unambiguous. +# S603 (subprocess call) and S607 (partial executable path). +# +# Every script in scripts/ is developer-run build/enumeration tooling whose JOB +# is to drive a native toolchain: clang-tidy, clang-format, cmake, git, xcrun. +# All 22 sites share one shape — a FIXED list-form argv with the default +# shell=False, so there is no shell to interpolate into, and the only +# non-literal arguments are paths the script itself computed from the repo root. +# None of it is untrusted or network input. S607 fires where the binary is +# resolved from PATH (`git`, `cmake`, `xcrun`, `clang-format`, `clang-tidy`), +# which here is deliberate: scripts/_env.sh pins tool VERSIONS by prepending to +# PATH, so PATH resolution IS the pinning mechanism, not a defect. +# +# Same rationale as the reference's "signalwire/cli/dokku.py" = ["S603","S607"]. +# Listed PER FILE rather than as a scripts/** blanket so a NEW subprocess call in +# a file that does not already shell out still reds the gate. +"**/_cpp_fmt.py" = ["S603", "S607"] +# E402 — module-level import not at top of file. Both sites in this file are +# STRUCTURALLY FORCED, not sloppiness, and are the same shape the reference +# grants its own conftest ("MUST insert the project root on sys.path BEFORE +# importing signalwire.*"): +# :167 `from clang.cindex import ...` must follow the +# `clang.cindex.Config.set_library_file(_LIBCLANG)` call above it -- +# importing those names first binds the wrong (or no) native libclang. +# :191 `from enumerate_surface import ...` must follow +# `sys.path.insert(0, str(HERE))`, which is what puts the sibling module +# on the path at all. +# Moving either import up does not make the file tidier; it makes it not work. +"**/enumerate_signatures.py" = ["S603", "S607", "E402"] +"**/enumerate_surface.py" = ["S603", "S607"] +"**/generate_rest_tests.py" = ["S603", "S607"] diff --git a/scripts/_cpp_fmt.py b/scripts/_cpp_fmt.py index aa0f65b..050353d 100644 --- a/scripts/_cpp_fmt.py +++ b/scripts/_cpp_fmt.py @@ -29,6 +29,7 @@ own output is already clean (it should have nothing to do). It is NOT on the normal emit path. """ + from __future__ import annotations import re @@ -56,6 +57,7 @@ def _repo_root() -> Path: # Deterministic formatter (the emit path). # --------------------------------------------------------------------------- + def _split_toplevel(param_str: str) -> list[str]: """Split a top-level comma list respecting <> and () nesting.""" out: list[str] = [] @@ -76,8 +78,9 @@ def _split_toplevel(param_str: str) -> list[str]: return out -def _binpack(first_line: str, params: list[str], tail: str, cont: str, - first_inline: bool) -> list[str]: +def _binpack( + first_line: str, params: list[str], tail: str, cont: str, first_inline: bool +) -> list[str]: """Bin-pack ``params`` (BinPackParameters/BinPackArguments). ``tail`` (e.g. ') const {' or ');') is attached to the LAST param for the fit decision so clang-format's break-before-last behaviour is reproduced. If ``first_inline`` the @@ -154,7 +157,9 @@ def _wrap_line(indent: str, s: str) -> list[str]: # method signature / function-call: () m = re.match(r"^(.*?\()(.*)(\)[^()]*)$", s) if m: - return _wrap_signature(indent, m.group(1), _split_toplevel(m.group(2)), m.group(3)) + return _wrap_signature( + indent, m.group(1), _split_toplevel(m.group(2)), m.group(3) + ) return [indent + s] @@ -165,11 +170,19 @@ def _collapse_short_functions(rows: list) -> list: i, n = 0, len(rows) while i < n: item = rows[i] - if (item != "" and item[1].endswith("{") and "(" in item[1] and ")" in item[1] - and not _CTRL.match(item[1]) - and i + 2 < n and rows[i + 1] != "" and rows[i + 2] != "" - and rows[i + 2][1] == "}" - and "{" not in rows[i + 1][1] and "}" not in rows[i + 1][1]): + if ( + item != "" + and item[1].endswith("{") + and "(" in item[1] + and ")" in item[1] + and not _CTRL.match(item[1]) + and i + 2 < n + and rows[i + 1] != "" + and rows[i + 2] != "" + and rows[i + 2][1] == "}" + and "{" not in rows[i + 1][1] + and "}" not in rows[i + 1][1] + ): ind, sig = item stmt = rows[i + 1][1] if len(f"{ind}{sig} {stmt} }}") <= COL: @@ -278,6 +291,7 @@ def format_generated_cpp(src: str) -> str: else: prev_blank = False lines.append(s) + # Rejoin continuation lines: if a code line has unbalanced '(' (a wrapped # signature/call split across lines), merge following lines until the parens # balance. This makes the formatter idempotent — feeding it already-wrapped input @@ -309,8 +323,12 @@ def _paren_balance(text: str) -> int: k += 1 continue bal = _paren_balance(cur) - while bal > 0 and k + 1 < len(lines) and lines[k + 1] != "" \ - and not lines[k + 1].lstrip().startswith("//"): + while ( + bal > 0 + and k + 1 < len(lines) + and lines[k + 1] != "" + and not lines[k + 1].lstrip().startswith("//") + ): k += 1 cur = cur + " " + lines[k] bal += _paren_balance(lines[k]) @@ -324,15 +342,22 @@ def _paren_balance(text: str) -> int: k = 0 while k < len(lines): cur = lines[k] - if (k + 1 < len(lines) and cur.startswith("explicit ") and cur.endswith(")") - and lines[k + 1].startswith(": ")): + if ( + k + 1 < len(lines) + and cur.startswith("explicit ") + and cur.endswith(")") + and lines[k + 1].startswith(": ") + ): init = lines[k + 1] k += 1 # absorb continuation initializer lines until the init list terminates # (a line ending in '{}' or '{' closes the constructor head). - while not (init.rstrip().endswith("{}") or init.rstrip().endswith("{")) \ - and k + 1 < len(lines) and lines[k + 1] != "" \ - and not lines[k + 1].lstrip().startswith("//"): + while ( + not (init.rstrip().endswith("{}") or init.rstrip().endswith("{")) + and k + 1 < len(lines) + and lines[k + 1] != "" + and not lines[k + 1].lstrip().startswith("//") + ): k += 1 init = init + " " + lines[k] joined.append(cur + " " + init) @@ -384,9 +409,8 @@ def _paren_balance(text: str) -> int: for ch in s: if ch == "{": stack.append("ns" if is_ns else "blk") - elif ch == "}": - if stack: - stack.pop() + elif ch == "}" and stack: + stack.pop() rows = _collapse_short_functions(rows) # wrap over-long code lines final: list[str] = [] @@ -409,6 +433,7 @@ def _paren_balance(text: str) -> int: # Verify-only backstop (NOT on the emit path). # --------------------------------------------------------------------------- + def clang_format_source(src: str, *, assume_filename: str = "x.hpp") -> str: """Return ``src`` formatted with the repo's clang-format config — the verify-only backstop (AGENT_RULES §5 level-2). ``format_generated_cpp`` already produces diff --git a/scripts/_env.sh b/scripts/_env.sh index 5230803..b676d9e 100755 --- a/scripts/_env.sh +++ b/scripts/_env.sh @@ -105,9 +105,118 @@ sw_build_jobs() { # compiler launcher (find_program(CCACHE_PROGRAM ccache) — a strict no-op when # absent, so the build never fails for a missing ccache). We only HINT here when # it isn't installed; we do NOT fail, because its absence must not break a build. -# Declared so it's present when wanted (CI declares it in porting-sdk's -# cross-port.yml cpp matrix install step; local devs get this hint). +# Declared so it's present when wanted, in BOTH CI layers per AGENT_RULES §7: +# porting-sdk's cross-port.yml cpp matrix install step AND this repo's own +# .github/workflows/{test,nightly}.yml (which are what actually run PR + nightly +# CI). Only cross-port.yml declared it until 2026-08-05, so every test.yml and +# nightly.yml runner printed the hint below and built fully cold — both workflows +# now apt-install ccache and persist ~/.cache/ccache across runs. if ! command -v ccache >/dev/null 2>&1; then echo "note: ccache not found — C++ rebuilds will be uncached (optional)." >&2 echo " Install it for near-instant warm rebuilds: brew install ccache" >&2 fi + +# Make ccache PATH-INSENSITIVE, for LOCAL runs and CI alike. +# +# ccache's fast "direct" mode keys on the absolute path of the translation unit, +# so a build at a new path misses direct and falls back to the slower +# preprocessed mode. PACKAGE-SMOKE builds in a PID-UNIQUE sandbox +# (porting-sdk package_smoke.py: .sw-tmp/package-smoke-cpp-), so its path +# differs on EVERY run BY CONSTRUCTION and direct mode could otherwise never hit. +# base_dir rewrites absolute paths beneath it to relative BEFORE hashing, and +# hash_dir=false keeps the cwd out of the hash — together they take the PID out of +# the cache SIGNATURE while leaving the sandbox's isolation untouched (that PID is +# load-bearing: package_smoke.py rm -rf's its own subtree, and a shared name would +# let concurrent runs delete each other's). +# +# Set HERE rather than only in the workflows so local and CI behave IDENTICALLY: +# this file is CWD-independent, whereas $GITHUB_WORKSPACE exists only on a runner — +# keying off that alone would silently give local devs preprocessed-only hits while +# CI got direct hits, i.e. the two environments would disagree about the cache. +# +# CHOOSING THE BASEDIR — DETECTED, NOT ASSUMED. base_dir only rewrites paths that +# live BENEATH it; anything outside stays absolute and misses direct mode. Using +# $REPO would bake in "every build dir is inside the port repo", which is an +# assumption about layout rather than a fact: gates scatter their scratch around +# (package_smoke.py builds in /.sw-tmp/package-smoke-cpp-, but +# ca_var_parity.py roots its scratch under the PORTING-SDK checkout instead), and +# the two checkouts sit side by side in both environments (~/src/{signalwire-cpp, +# porting-sdk} locally, $GITHUB_WORKSPACE/{signalwire-cpp,porting-sdk} on a runner). +# So derive the basedir as the deepest COMMON ANCESTOR of this repo and the +# porting-sdk checkout when we can see both, which covers scratch under either one, +# and fall back to $REPO when we cannot. Nothing here hardcodes a directory name or +# a nesting depth. +# +# Existing values always win, so a caller can override. +if [ -z "${CCACHE_BASEDIR:-}" ]; then + _cc_base="$REPO" + # Locate the porting-sdk checkout without assuming where it is: honour an + # explicit $PORTING_SDK, else look for a sibling of the repo. + _cc_psdk="${PORTING_SDK:-}" + if [ -z "$_cc_psdk" ] && [ -d "$(dirname "$REPO")/porting-sdk" ]; then + _cc_psdk="$(dirname "$REPO")/porting-sdk" + fi + if [ -n "$_cc_psdk" ] && [ -d "$_cc_psdk" ]; then + # Deepest common ancestor of $REPO and $_cc_psdk, computed by walking up. + _cc_a="$(cd "$REPO" && pwd -P)" + _cc_b="$(cd "$_cc_psdk" && pwd -P)" + while [ -n "$_cc_a" ] && [ "$_cc_a" != "/" ]; do + case "$_cc_b/" in + "$_cc_a"/*) _cc_base="$_cc_a"; break ;; + esac + _cc_a="$(dirname "$_cc_a")" + done + unset _cc_a _cc_b + fi + export CCACHE_BASEDIR="$_cc_base" + unset _cc_base _cc_psdk +fi +export CCACHE_NOHASHDIR="${CCACHE_NOHASHDIR:-1}" + +# Measured (full Release build of the library, 130 TUs): +# cold at path A 158.4s +# rebuild at a DIFFERENT path B 1.5s 130/130 DIRECT hits +# versus 32.0s / 89-of-260 preprocessed-only hits without these. +# +# Correctness negative-controlled in all three directions: a changed source +# recompiles, a changed HEADER recompiles, and a build from a different cwd +# reuses the right object with __FILE__ intact — no false hits. + +# --- ctcache (clang-tidy result cache) --------------------------------------- +# run-lint.sh routes clang-tidy through scripts/clang_tidy_cache.py ONLY when +# $CTCACHE_DIR is set (else: plain clang-tidy, exact prior behaviour). That var +# was exported by the CI workflows and nowhere else, so CI got the cache and a +# LOCAL `run-lint.sh` / `run-ci.sh` always re-ran clang-tidy from scratch — the +# same local-vs-CI asymmetry the ccache block above exists to prevent. Measured on +# this machine: a full local LINT is ~378s uncached, while CI's cached LINT is +# 14-27s. Default it here so the two environments cache alike; the CI workflows +# still export their own value (pointing at the dir actions/cache persists), and +# an explicit value always wins. +# +# XDG_CACHE_HOME is honoured when set, else ~/.cache — matching where the CI +# workflow puts it and where ccache defaults, rather than inventing a new dir. +# ctcache does NOT expanduser its value, so this must be an ABSOLUTE path. +export CTCACHE_DIR="${CTCACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/ctcache}" + +# ruff — the PY-LINT gate's linter/formatter for the hand-written Python under +# scripts/. A HINT here rather than a hard failure, because the C++ build/test +# path does not need it; the gate itself (scripts/run-pylint.sh) fails loud with +# the same hint when it is actually required. Declared in BOTH layers per +# AGENT_RULES §7 — here for local devs, and as a pip install in the CI workflow +# next to the pinned clang-format — so a fresh clone or a CI runner has it. +# +# PINNED EXACT, for the same reason clang-format is pinned to 18 above: an +# unbounded linter version is a green-locally/red-in-CI generator. CI installs the +# newest release at run time while a local dev runs whatever they installed months +# ago, so a ruff release that adds a rule or changes a format heuristic reds +# PY-LINT on code that never changed. run-pylint.sh ASSERTS this version, exactly +# as this file asserts clang-format major 18. Keep in lockstep with the +# `pip install "ruff==…"` in .github/workflows/{test,nightly}.yml; 0.15.21 is the +# fleet-wide ruff (python/perl/php/typescript/java pin the same). +SW_RUFF_VERSION="0.15.21" +export SW_RUFF_VERSION + +if ! command -v ruff >/dev/null 2>&1; then + echo "note: ruff not found — the PY-LINT gate (scripts/*.py) cannot run." >&2 + echo " Install it with: pip install ruff==$SW_RUFF_VERSION" >&2 +fi diff --git a/scripts/enumerate_signatures.py b/scripts/enumerate_signatures.py index bc2e278..2ff1ccc 100644 --- a/scripts/enumerate_signatures.py +++ b/scripts/enumerate_signatures.py @@ -37,6 +37,7 @@ import os as _os import sysconfig as _sc + def _macos_llvm_prefixes() -> list[str]: """Candidate Homebrew LLVM install prefixes on macOS, newest-pinned first. @@ -47,10 +48,9 @@ def _macos_llvm_prefixes() -> list[str]: prefixes: list[str] = [] # `brew --prefix llvm` / versioned kegs, without requiring brew on PATH: # probe the standard Apple-silicon and Intel Cellar/opt layouts. - import glob as _glob for base in ("/opt/homebrew/opt", "/usr/local/opt"): # Prefer an explicitly versioned keg (llvm@18, llvm@19, …) then plain llvm. - prefixes += sorted(_glob.glob(f"{base}/llvm@*"), reverse=True) + prefixes += sorted((str(q) for q in Path(base).glob("llvm@*")), reverse=True) prefixes.append(f"{base}/llvm") return prefixes @@ -67,7 +67,6 @@ def _macos_clang_args(libclang_path: str | None) -> list[str]: Derived from the same LLVM prefix that provided libclang.dylib so the dylib and headers are one self-consistent toolchain. """ - import glob as _glob args: list[str] = [] try: sdk = subprocess.run( @@ -76,7 +75,10 @@ def _macos_clang_args(libclang_path: str | None) -> list[str]: if sdk and Path(sdk).is_dir(): args += ["-isysroot", sdk] except (OSError, subprocess.CalledProcessError) as e: - print(f"enumerate_signatures: xcrun --show-sdk-path failed ({e})", file=sys.stderr) + print( + f"enumerate_signatures: xcrun --show-sdk-path failed ({e})", file=sys.stderr + ) + # Find a libc++ + builtin (resource) header pair. Prefer the prefix that # provided libclang.dylib (a self-consistent toolchain); but the pip # `libclang` package's dylib lives in clang/native/ with NO headers — in @@ -84,11 +86,16 @@ def _macos_clang_args(libclang_path: str | None) -> list[str]: # doesn't silently degrade types to `int`. def _libcxx_and_builtins(prefix: Path): libcxx = prefix / "include" / "c++" / "v1" - builtins = sorted(_glob.glob(str(prefix / "lib" / "clang" / "*" / "include")), reverse=True) + builtins = sorted( + (str(q) for q in (prefix / "lib" / "clang").glob("*/include")), reverse=True + ) return (str(libcxx), builtins[0]) if libcxx.is_dir() and builtins else None + found = None if libclang_path: - found = _libcxx_and_builtins(Path(libclang_path).parent.parent) # /lib/libclang.dylib + found = _libcxx_and_builtins( + Path(libclang_path).parent.parent + ) # /lib/libclang.dylib if not found: for prefix in _macos_llvm_prefixes(): found = _libcxx_and_builtins(Path(prefix)) @@ -98,9 +105,11 @@ def _libcxx_and_builtins(prefix: Path): libcxx, builtins = found args += ["-nostdinc++", "-isystem", libcxx, "-isystem", builtins] else: - print("enumerate_signatures: no matched libc++/builtin headers found on " - "macOS (install a Homebrew `llvm` keg); C++ types may degrade to int", - file=sys.stderr) + print( + "enumerate_signatures: no matched libc++/builtin headers found on " + "macOS (install a Homebrew `llvm` keg); C++ types may degrade to int", + file=sys.stderr, + ) return args @@ -155,12 +164,15 @@ def _find_libclang() -> str | None: "/usr/lib/x86_64-linux-gnu/libclang-15.so.1", # Local-dev fallback: the clang python bindings' bundled native lib, # derived from $HOME so it is machine-agnostic. - str(Path.home() / ".local/lib/python3.12/site-packages/clang/native/libclang.so"), + str( + Path.home() / ".local/lib/python3.12/site-packages/clang/native/libclang.so" + ), ): if Path(cand).is_file(): return cand return None + _LIBCLANG = _find_libclang() if _LIBCLANG: clang.cindex.Config.set_library_file(_LIBCLANG) @@ -190,9 +202,17 @@ def _resolve_psdk() -> Path: sys.path.insert(0, str(HERE)) from enumerate_surface import ( # type: ignore CALLBACK_TYPEDEFS_AS_CALLABLE, - CLASS_MODULE_MAP, CLASS_RENAME_MAP, FREE_FUNCTION_RENAMES, - MIXIN_PROJECTIONS, _METHOD_RENAMES, - camel_to_snake, module_for_class, native_ns_to_module, + CLASS_MODULE_MAP, + CLASS_RENAME_MAP, + FREE_FUNCTION_RENAMES, + MIXIN_PROJECTIONS, + SKILL_SOURCE_DIR, + _METHOD_RENAMES, + camel_to_snake, + module_for_class, + native_ns_to_module, + strip_block_comments, + strip_line_comments, ) # Methods whose canonical name should resolve to the OVERLOAD WITH THE MOST @@ -297,10 +317,10 @@ def translate_cpp_type(t: str, aliases: dict[str, str], context: str) -> str: new_t = t for prefix in ("const ", "volatile ", "constexpr "): if new_t.startswith(prefix): - new_t = new_t[len(prefix):].strip() + new_t = new_t[len(prefix) :].strip() for suffix in ("&&", "&", "*"): if new_t.endswith(suffix): - new_t = new_t[:-len(suffix)].strip() + new_t = new_t[: -len(suffix)].strip() if new_t == t: break t = new_t @@ -331,8 +351,7 @@ def translate_cpp_type(t: str, aliases: dict[str, str], context: str) -> str: if head in ("std::optional", "boost::optional"): return f"optional<{canon_args[0]}>" if canon_args else "optional" if head in ("std::shared_ptr", "std::unique_ptr", "std::weak_ptr"): - inner_canon = canon_args[0] if canon_args else "any" - return inner_canon + return canon_args[0] if canon_args else "any" if head in ("std::function",): # std::function if canon_args: @@ -343,7 +362,10 @@ def translate_cpp_type(t: str, aliases: dict[str, str], context: str) -> str: args_part = args_part[:-1] ret = translate_cpp_type(ret_part, aliases, context) if args_part.strip(): - canon_a = [translate_cpp_type(a, aliases, context) for a in split_top_commas(args_part)] + canon_a = [ + translate_cpp_type(a, aliases, context) + for a in split_top_commas(args_part) + ] else: canon_a = [] return f"callable,{ret}>" @@ -388,7 +410,7 @@ def _build_rename_by_name() -> dict[str, tuple[str, str]]: multiple namespaces in the map, prefer the first registration. """ by_name: dict[str, tuple[str, str]] = {} - for (ns, cls_name), (mod, py_cls) in CLASS_RENAME_MAP.items(): + for (_ns, cls_name), (mod, py_cls) in CLASS_RENAME_MAP.items(): by_name.setdefault(cls_name, (mod, py_cls)) return by_name @@ -437,8 +459,7 @@ def _translate_sdk_class_ref(t: str) -> str: # table keys on ``(signalwire::rest, AddressesNamespace)``. ns_candidates = [ns_path] if ns_path else [] parts = ns_path.split("::") if ns_path else [] - for i in range(len(parts) - 1, 0, -1): - ns_candidates.append("::".join(parts[:i])) + ns_candidates.extend("::".join(parts[:i]) for i in range(len(parts) - 1, 0, -1)) for ns in ns_candidates: if (ns, name) in CLASS_RENAME_MAP: target_mod, target_cls = CLASS_RENAME_MAP[(ns, name)] @@ -454,7 +475,11 @@ def _translate_sdk_class_ref(t: str) -> str: mod = module_for_class(name, ns_path) if mod: return f"class:{mod}.{name}" - return f"class:signalwire.{native_ns_to_module(ns_path)}.{name}" if ns_path else f"class:{name}" + return ( + f"class:signalwire.{native_ns_to_module(ns_path)}.{name}" + if ns_path + else f"class:{name}" + ) # --------------------------------------------------------------------------- @@ -463,7 +488,8 @@ def _translate_sdk_class_ref(t: str) -> str: def walk_translation_unit( - tu: TranslationUnit, file_filter: Path, + tu: TranslationUnit, + file_filter: Path, ) -> tuple[list[dict], list[dict], dict[str, list[dict]]]: """Walk a clang TU and emit (class entries, free-function entries). @@ -477,7 +503,7 @@ def walk_translation_unit( def visit(cursor, ns_path: list[str]): if cursor.kind in (CursorKind.NAMESPACE,): - new_ns = ns_path + [cursor.spelling] + new_ns = [*ns_path, cursor.spelling] for child in cursor.get_children(): visit(child, new_ns) return @@ -495,27 +521,16 @@ def visit(cursor, ns_path: list[str]): fname = cursor.spelling if not fname or fname.startswith("_"): return - params = [] - for arg in cursor.get_arguments(): - params.append({ - "name": arg.spelling, - "type": arg.type.spelling, - # Carry the canonical spelling alongside the - # typedef-aware spelling so the translator can fall - # back to it when a typedef (e.g. ``ParamsOrBody`` - # over ``std::variant<...>``) is opaque to its - # bare-name lookup. Class methods use the same trick - # via ``extract_method``. - "canonical_type": arg.type.get_canonical().spelling, - "has_default": _has_default_value(arg), - }) - free_functions.append({ - "namespace": "::".join(ns_path), - "name": fname, - "parameters": params, - "return_type": cursor.result_type.spelling, - "canonical_return_type": cursor.result_type.get_canonical().spelling, - }) + params = [_param_record(arg) for arg in cursor.get_arguments()] + free_functions.append( + { + "namespace": "::".join(ns_path), + "name": fname, + "parameters": params, + "return_type": cursor.result_type.spelling, + "canonical_return_type": cursor.result_type.get_canonical().spelling, + } + ) return if cursor.kind in (CursorKind.CLASS_DECL, CursorKind.STRUCT_DECL): if not cursor.is_definition(): @@ -545,11 +560,13 @@ def visit(cursor, ns_path: list[str]): fname = child.spelling if not fname or fname.startswith("_") or fname.endswith("_"): continue - fields.append({ - "name": fname, - "type": child.type.spelling, - "canonical_type": child.type.get_canonical().spelling, - }) + fields.append( + { + "name": fname, + "type": child.type.spelling, + "canonical_type": child.type.get_canonical().spelling, + } + ) for child in cursor.get_children(): if child.kind == CursorKind.CXX_METHOD: if child.access_specifier.name != "PUBLIC": @@ -586,13 +603,33 @@ def visit(cursor, ns_path: list[str]): # and thereby inventing an inventory class. if fields: options_structs[f"{ns_str}::{class_name}"] = fields - if methods: - entries.append({ + # A fields-only POD is admitted to the inventory ONLY when the + # reference ORACLE records a class of that name in the module this + # class maps to — i.e. the reference genuinely has this class and + # spells its whole surface as attributes. That is the case for the + # credential carriers (``BasicCredentials``/``BearerCredentials``, + # two std::string fields and no methods at all): the reference + # records them as dataclasses whose members ARE the fields, so + # dropping them here made an implemented carrier read as + # missing-port drift. The oracle gate is what keeps this from + # inventing an inventory class out of an internal options struct + # (``RelayConfig`` has no reference counterpart and stays out). + if ( + not methods + and fields + and _oracle_records_class(ns_str, class_name, str(fn.name)) + ): + methods = [] + elif not methods: + return + entries.append( + { "namespace": ns_str, "name": class_name, "methods": methods, "fields": fields, - }) + } + ) return # Recurse into other top-level structures for child in cursor.get_children(): @@ -615,35 +652,57 @@ def _is_copy_or_move_ctor(cursor) -> bool: try: if fn(): return True - except Exception: - pass + except (AttributeError, TypeError, ValueError) as e: + # The binding exposes the name but cannot evaluate it (older + # libclang builds raise instead of returning False). That is + # precisely what the structural fallback below exists for, so + # carry on -- but SAY which predicate was unusable: a silent + # pass here is indistinguishable from "the predicate said no", + # and the two have very different consequences for the + # ctor/dunder classification this function drives. + print( + f"enumerate_signatures: libclang {pred}() unusable ({e}); " + "falling back to the structural copy/move test", + file=sys.stderr, + ) args = list(cursor.get_arguments()) if len(args) != 1: return False t = args[0].type.get_canonical().spelling for prefix in ("const ", "volatile "): while t.startswith(prefix): - t = t[len(prefix):] + t = t[len(prefix) :] t = t.rstrip("&").strip() for prefix in ("const ", "volatile "): while t.startswith(prefix): - t = t[len(prefix):] + t = t[len(prefix) :] return t.rsplit("::", 1)[-1] == cursor.semantic_parent.spelling +def _param_record(arg) -> dict: + """Raw parameter record for one PARM_DECL cursor. + + ``canonical_type`` carries the typedef-EXPANDED spelling alongside the + typedef-aware one so the translator can fall back to it when a port-internal + typedef (e.g. ``ParamsOrBody`` over ``std::variant<...>``) is opaque to its + bare-name lookup. See ``_translate_with_canonical_fallback``. + + ``default_value`` is the parsed default-argument literal, or ``_NO_DEFAULT`` + when the parameter has no default OR its default is a non-literal expression; + ``has_default`` distinguishes those two cases. + """ + has_default, default_value = _extract_default(arg) + return { + "name": arg.spelling, + "type": arg.type.spelling, + "canonical_type": arg.type.get_canonical().spelling, + "has_default": has_default, + "default_value": default_value, + } + + def extract_method(cursor, is_ctor: bool) -> dict: - params = [] - for arg in cursor.get_arguments(): - params.append({ - "name": arg.spelling, - "type": arg.type.spelling, - # Carry the canonical (typedef-expanded) spelling so the - # translator can fall back to it when a port-internal - # typedef hides a known type. See - # ``_translate_with_canonical_fallback``. - "canonical_type": arg.type.get_canonical().spelling, - "has_default": _has_default_value(arg), - }) + params = [_param_record(arg) for arg in cursor.get_arguments()] return { "name": "" if is_ctor else cursor.spelling, "is_constructor": is_ctor, @@ -656,13 +715,634 @@ def extract_method(cursor, is_ctor: bool) -> dict: } -def _has_default_value(arg) -> bool: - """Heuristic: scan tokens for '=' after the arg name.""" +# Sentinel distinguishing "this parameter has NO default" from "it has a default +# whose value we could not reduce to a JSON literal". ``None`` cannot serve for +# either, because ``None`` is also the value we emit for a genuine ``nullptr`` / +# ``std::nullopt`` default. +_NO_DEFAULT = object() + + +def _default_tokens(arg) -> list[str] | None: + """Token spellings of a parameter's default-argument EXPRESSION, or None. + + libclang's Python binding has no ``clang_getParmDeclDefaultArgument``, but the + PARM_DECL cursor's own token extent covers the whole `` = `` + declaration, so the default expression is recoverable as the tokens after the + parameter's top-level ``=``. + + "Top-level" is load-bearing: the ``=`` must be located at zero bracket depth so + a template argument list (``std::map m = {}``) or a nested + ``<...>`` cannot be mistaken for the assignment. + + Depth is counted PER CHARACTER, not per token, because libclang emits a nested + template close as the SINGLE token ``>>`` (and ``>>>`` for triple nesting) — + the C++ right-shift spelling. Decrementing once per token left the depth + permanently positive for every ``std::optional>`` + parameter, so its ``=`` was never seen at top level and a real default was + silently reported as NO default (flipping ``required`` to true on 32 params). + Likewise ``->`` / ``<=`` / ``>=`` must not be counted as brackets at all. + """ try: - tokens = list(arg.get_tokens()) + tokens = [t.spelling for t in arg.get_tokens()] except Exception: + return None + # Multi-character operator tokens that CONTAIN an angle bracket but are not + # template punctuation. Checked before the per-character bracket count. + _NON_BRACKET_OPS = {"->", "->*", "<=", ">=", "<=>", "==", "!=", "<<", "&&"} + depth = 0 + for i, tok in enumerate(tokens): + if tok == "=" and depth == 0: + rest = tokens[i + 1 :] + return rest or None + if tok in _NON_BRACKET_OPS: + continue + for ch in tok: + if ch in "<([{": + depth += 1 + elif ch in ">)]}": + depth -= 1 + return None + + +# C++ default expressions that mean "absent" and translate to a JSON null. These +# are REAL defaults — a parameter carrying one is ``required: false`` with an +# explicit null, which is NOT the same as a parameter with no default at all. +_NULLISH_DEFAULTS = { + ("nullptr",), + ("NULL",), + ("std", "::", "nullopt"), + ("nullopt",), +} + +# Empty brace-init ``= {}`` — a real default meaning "value-initialized". Its JSON +# form depends on the parameter's type, resolved by the caller. +_EMPTY_BRACE = ("{", "}") + +_INT_RE = re.compile(r"^[+-]?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\d+)[uUlL]*$") +_FLOAT_RE = re.compile(r"^[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[fFlL]?$") + + +def _parse_cpp_literal(tokens: list[str]): + """Reduce a C++ default-argument token list to a JSON-comparable value. + + Returns ``_NO_DEFAULT`` when the expression is NOT a static literal — an enum + value, a constructor call, an arithmetic expression, a named constant. Those + are recorded as ``default: null`` by the caller rather than guessed at: the + reference records concrete values, and inventing one here would manufacture a + confident wrong answer, which is worse than a documented blind spot. + """ + if not tokens: + return _NO_DEFAULT + + if tuple(tokens) in _NULLISH_DEFAULTS: + return None + + # String literals: one or more adjacent literals, concatenated as C++ does. + # Covers ``""`` (empty string) and ``"a" "b"``. Only plain/UTF-8 literals with + # no escape sequences beyond the common ones are decoded; anything exotic + # falls through to non-literal. + if all(t.startswith('"') and t.endswith('"') and len(t) >= 2 for t in tokens): + out = [] + for t in tokens: + body = t[1:-1] + try: + out.append(json.loads('"' + body + '"')) + except ValueError: + return _NO_DEFAULT + return "".join(out) + + if len(tokens) == 1: + tok = tokens[0] + if tok == "true": + return True + if tok == "false": + return False + if _INT_RE.match(tok): + digits = tok.rstrip("uUlL") + try: + return int(digits, 0) + except ValueError: + return _NO_DEFAULT + if _FLOAT_RE.match(tok) and ( + "." in tok or "e" in tok.lower() or tok[-1] in "fF" + ): + try: + return float(tok.rstrip("fFlL")) + except ValueError: + return _NO_DEFAULT + # A bare char literal, an identifier (named constant / enum), etc. + return _NO_DEFAULT + + # Unary sign applied to a numeric literal: ``= -1``, ``= +2.5``. + if len(tokens) == 2 and tokens[0] in ("-", "+"): + inner = _parse_cpp_literal([tokens[1]]) + if isinstance(inner, (int, float)) and not isinstance(inner, bool): + return -inner if tokens[0] == "-" else inner + return _NO_DEFAULT + + # Everything else (``Color::Red``, ``std::string("x")``, ``60 * 60``, + # ``Opt{}``, ``SomeConstant``) is a non-literal expression. + return _NO_DEFAULT + + +# Canonical types whose ``= {}`` value-initialization has a well-defined JSON form. +# A ``{}`` default on any other type (an SDK class, a struct) is a constructed +# object, not a literal, and is left non-literal. +def _empty_brace_default(canon_type: str): + t = (canon_type or "").strip() + if t.startswith("list<"): + return [] + if t.startswith("dict<"): + return {} + if t == "string": + return "" + if t == "bool": + return False + if t in ("int", "float"): + return 0 if t == "int" else 0.0 + if t.startswith("optional<") or t == "any": + return None + return _NO_DEFAULT + + +def _extract_default(arg): + """(has_default, value) for one PARM_DECL cursor. + + ``value`` is ``_NO_DEFAULT`` when a default exists but is a non-literal + expression we deliberately refuse to evaluate. + """ + tokens = _default_tokens(arg) + if tokens is None: + return False, _NO_DEFAULT + if tuple(tokens) == _EMPTY_BRACE: + return True, _EMPTY_BRACE + return True, _parse_cpp_literal(tokens) + + +def _has_default_value(arg) -> bool: + """True when the parameter declares a default argument (value aside).""" + return _default_tokens(arg) is not None + + +# --------------------------------------------------------------------------- +# The null <-> zero-value sentinel fold (the C++ "no nullable scalars" idiom) +# --------------------------------------------------------------------------- +# +# THE VOCABULARY RULE +# =================== +# Python expresses "the caller did not supply this" as ``x: T | None = None`` and +# then guards the wire with ``if x is not None:``. C++ has no nullable scalar and +# no keyword arguments, so the SAME contract is spelled as a ZERO-VALUE SENTINEL +# default plus an absence guard in the body: +# +# python def user_event(self, event: str | None = None) if event is not None: p["event"] = event +# cpp Action user_event(const std::string& event = "") if (!event.empty()) { p["event"] = event; } +# +# Those two are behaviourally identical: a caller who omits the argument produces +# the identical wire frame in both languages. Recording the C++ side as +# ``default: ""`` while the reference records ``default: null`` manufactures drift +# out of two spellings of "absent". +# +# So the enumerator FOLDS the sentinel to ``null`` — at the emitter, in the +# canonical vocabulary, so the comparison keeps running (an allow-list would stop +# comparing and blind the gate to a real value change). +# +# THE FOLD IS EVIDENCE-GATED. It is NOT "empty string always means null". A port +# that defaults ``prompt=""`` and then SENDS ``prompt: ""`` ships a different +# request body than a reference that omits the key, and that is a REAL divergence +# the gate must keep reporting. The fold therefore requires BOTH: +# +# 1. the default is the parameter type's ZERO VALUE / documented sentinel +# (table below), AND +# 2. a GUARD in the method's definition body that tests that sentinel and +# suppresses the value's use. +# +# No guard -> no fold. The sentinel is then a value the port genuinely ships, and +# the ``default-mismatch`` finding stands as a real one. +# +# type sentinel guard that proves absence +# --------------- ----------- ----------------------------------------- +# std::string "" !p.empty() / p.empty() ? ... : p / p != "" +# vector/map/json {} !p.empty() / p.empty() ? ... +# integral 0, -1 p > 0 / p >= 0 / p != 0 / p != -1 +# floating -1.0, 0.0 p >= 0.0 / p > 0.0 / p != -1.0 +# optional/pointer nullopt/null (already recorded as null; nothing to fold) +# +# ONE TRANSITIVE HOP — AND NOT FOR STRINGS. Several methods store the sentinel on +# a member and guard it at SERIALIZATION rather than at the entry point — e.g. +# ``Step::set_gather_info`` assigns ``GatherInfo(output_key, ...)`` and +# ``GatherInfo::to_json()`` then does ``if (!output_key_.empty())``. The guard is +# still the proof that the sentinel never reaches the wire, so a member-name guard +# (``_`` or ````, the repo's member spelling) anywhere in the SAME +# source file counts. Exactly one hop; the scanner never chases further, so an +# unproven chain reports drift rather than folding on a guess. +# +# The transitive hop is DISALLOWED for the ``""`` sentinel, and this is the whole +# reason the string case needs its own rule. Measured against the oracle +# (2026-07-27): ``[]`` and ``{}`` NEVER appear as a reference default — 0 of +# 1,505 recorded defaults — so an empty container in the reference is always +# ``None`` and a guarded empty-container sentinel is unambiguously "absent". +# ``""``, by contrast, is a REAL reference default 111 times, and ``0``/``0.0`` +# 35 times. An empty string is a value Python genuinely sends. +# +# What separates the two IS visible on the port side: the C++ code models the +# distinction in its STORAGE type, the same way the reference models it in its +# annotation. ``pom::Section`` declares ``std::optional title`` +# (reference: ``str | None = None``) next to ``std::string body`` (reference: +# ``str = ""``) — the port and the reference agree, independently. A parameter +# stored verbatim onto a non-optional member and only guarded later at +# serialization (``body``) is a real empty-string default; a parameter the method +# itself tests before use (``event``, ``status_url``) models absence. +# +# So: strings fold ONLY on a DIRECT guard in the method's own body. Without that +# split the fold turns the 5 POM ``body`` parameters into nulls, inventing a +# ``"" vs null`` mismatch in the other direction — which is the same class of +# error as not folding at all, just pointing the other way. +# +# WHAT THIS RULE DOES NOT PROVE, stated plainly so the next reader does not +# mistake it for stronger than it is: +# +# * NUMERICS keep the transitive hop even though ``0`` is also a real reference +# default (35 times). Every numeric fold this produces today is a ``timeout`` +# / ``volume`` / ``max_duration`` parameter with a ``> 0`` guard and a +# reference ``None``, verified param-by-param — the hop is simply not +# load-bearing for any of them. If a future ``0``-defaulted numeric ever +# folds WRONG, tighten it to direct-guard-only the way strings already are. +# * A TRANSITIVELY-guarded STRING that the reference really does declare +# ``str | None`` (``Step::set_gather_info``'s three parameters) is NOT folded +# and keeps reporting drift. That is the rule choosing a false NEGATIVE over +# a false positive: the port stores those in plain ``std::string`` members, +# which is indistinguishable from the ``body`` shape. Closing them means +# changing the PORT to model absence (``std::optional``, as +# ``pom::Section::title`` already does), not loosening this rule. +# * The hop is scoped to ONE source file. A parameter stored into a member +# declared and guarded in a different translation unit (``AgentBase:: +# prompt_add_section``'s ``bullets``, guarded in ``pom.cpp``) does not fold. +# Widening to whole-tree matching would let any ``.empty()`` anywhere satisfy +# the guard, which is not evidence. + +# Sentinel default VALUES that are foldable per type, keyed by the canonical +# (translated) type prefix. A value not in this table is a real default and is +# never folded, however guarded the parameter is. +_FOLDABLE_SENTINELS: dict[str, tuple] = { + "string": ("",), + "list": ([],), + "dict": ({},), + "int": (0, -1), + "float": (0.0, -1.0), +} + + +def _sentinel_kind(canon_type: str) -> str | None: + t = (canon_type or "").strip() + if t == "string": + return "string" + if t.startswith("list<"): + return "list" + if t.startswith("dict<") or t == "any": + # ``any`` is the translated form of nlohmann::json, whose ``= {}`` / + # empty-object default is guarded with ``.empty()`` exactly like a map. + return "dict" + if t == "int": + return "int" + if t == "float": + return "float" + return None + + +def _is_foldable_sentinel(canon_type: str, value) -> bool: + kind = _sentinel_kind(canon_type) + if kind is None: + return False + if isinstance(value, bool): + # ``bool`` has no "absent" spelling: false is a real, sendable value. + return False + for sentinel in _FOLDABLE_SENTINELS[kind]: + if type(sentinel) is type(value) and sentinel == value: + return True + if ( + kind == "float" + and isinstance(value, (int, float)) + and float(sentinel) == float(value) + ): + return True + return False + + +# Guard shapes, per sentinel kind, rendered against a parameter NAME placeholder. +# Each is matched against the definition body with the name substituted in. +_GUARD_PATTERNS: dict[str, list[str]] = { + # ``!p.empty()`` / ``p.empty() ?`` / ``!p.is_null()`` / ``p != ""`` + "string": [ + r"!\s*{n}\s*\.empty\s*\(\s*\)", + r"\b{n}\s*\.empty\s*\(\s*\)\s*\?", + r"\b{n}\s*!=\s*\"\"", + r"\bif\s*\(\s*{n}\s*\.empty\s*\(\s*\)\s*\)", + ], + "list": [ + r"!\s*{n}\s*\.empty\s*\(\s*\)", + r"\b{n}\s*\.empty\s*\(\s*\)\s*\?", + r"\bif\s*\(\s*{n}\s*\.empty\s*\(\s*\)\s*\)", + ], + "dict": [ + r"!\s*{n}\s*\.empty\s*\(\s*\)", + r"!\s*{n}\s*\.is_null\s*\(\s*\)", + r"\b{n}\s*\.empty\s*\(\s*\)\s*\?", + r"\b{n}\s*\.is_object\s*\(\s*\)\s*\?", + r"\bif\s*\(\s*{n}\s*\.empty\s*\(\s*\)\s*\)", + ], + "int": [ + r"\b{n}\s*(?:>|>=|!=|==)\s*[-+]?\d", + r"\bif\s*\(\s*{n}\s*\)", + ], + "float": [ + r"\b{n}\s*(?:>|>=|!=|==)\s*[-+]?[\d.]", + ], +} + + +def _member_spellings(param_name: str) -> list[str]: + """Names a parameter may have been stored under for the ONE transitive hop. + + The repo's convention is a trailing-underscore private member + (``output_key`` -> ``output_key_``); a handful store under the bare name. + """ + return [param_name + "_", param_name] + + +def _param_names_from_list(param_list: str) -> list[str]: + """Parameter NAMES, in order, from a definition's parameter-list text. + + ``param_list`` is everything between the ``(`` and the body's ``{``, minus the + leading ``(`` the caller already consumed. Split on top-level commas (so a + ``std::map`` or a ``std::variant`` argument is one parameter, not + two) and take the trailing identifier of each part — C++ puts the declarator + name last, after any ``const``/``&``/template spelling. + """ + body_end = param_list.rfind(")") + inner = param_list[:body_end] if body_end >= 0 else param_list + parts: list[str] = [] + depth, buf = 0, [] + for ch in inner: + if ch in "<([{": + depth += 1 + elif ch in ">)]}": + depth -= 1 + if ch == "," and depth == 0: + parts.append("".join(buf)) + buf = [] + continue + buf.append(ch) + parts.append("".join(buf)) + names: list[str] = [] + ident = re.compile(r"([A-Za-z_]\w*)\s*(?:=[^,]*)?$") + for part in parts: + # Drop a default-argument expression and any trailing array extent, then + # take the last identifier. + head = part.split("=", 1)[0].strip().rstrip("[]").strip() + m = ident.search(head) + names.append(m.group(1) if m else "") + return names + + +_FORWARD_RE = re.compile( + r"^\{\s*return\s+(?:[A-Za-z_]\w*::)*([A-Za-z_]\w*)\s*\((.*)\)\s*;\s*\}$", re.S +) + + +def _pure_forward_target(body: str) -> tuple[str, list[str]] | None: + """``(callee_name, [argument spellings])`` when ``body`` is one ``return f(...);``. + + Only a body whose ENTIRE content is that single statement qualifies — + comments are stripped first, but any additional statement disqualifies it. + Arguments are split on top-level commas and kept verbatim, so the caller can + match a parameter by NAME and recover its position in the callee. + """ + stripped = re.sub(r"//[^\n]*", "", body) + stripped = re.sub(r"/\*.*?\*/", "", stripped, flags=re.S) + stripped = " ".join(stripped.split()) + m = _FORWARD_RE.match(stripped) + if not m: + return None + args, depth, buf = [], 0, [] + for ch in m.group(2): + if ch in "<([{": + depth += 1 + elif ch in ">)]}": + depth -= 1 + if ch == "," and depth == 0: + args.append("".join(buf).strip()) + buf = [] + continue + buf.append(ch) + args.append("".join(buf).strip()) + return m.group(1), args + + +class GuardIndex: + """Which parameters of which C++ methods carry an absence guard. + + Built by a text scan of the implementation tree (``src/**/*.cpp``) plus the + headers' inline bodies. libclang is not used here on purpose: the enumerator + parses headers with ``PARSE_SKIP_FUNCTION_BODIES`` (a 3-10x speedup on the + SIGNATURES gate), and re-parsing all 67 translation units to read bodies + would give back that entire saving to answer a question a brace-matched text + scan answers exactly as well. + + Keyed by ``(ClassName, methodName)``; a class's method may be defined in more + than one file (and a method may be overloaded), so every matching body is + unioned — a guard in ANY definition of that name proves the port models the + absence. + """ + + def __init__(self, roots: list[Path]): + # (class, method) -> list[(body_text, file_text, [param names in order])] + self._bodies: dict[tuple[str, str], list[tuple[str, str, list[str]]]] = {} + # Free-function definitions, keyed by bare name — the forwarding-callee + # lookup below. Same (body, file_text, names) shape as _bodies. + self._free: dict[str, list[tuple[str, str, list[str]]]] = {} + self._defpat = re.compile(r"\b([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*\(", re.M) + self._freepat = re.compile( + r"^[A-Za-z_][\w:<>,\s*&]*?\b([A-Za-z_]\w*)\s*\(", re.M + ) + for root in roots: + if not root.is_dir(): + continue + for path in sorted(root.rglob("*.cpp")) + sorted(root.rglob("*.hpp")): + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + self._index_file(text) + + def _index_file(self, text: str) -> None: + for m in self._defpat.finditer(text): + cls, method = m.group(1), m.group(2) + open_brace = text.find("{", m.end()) + if open_brace < 0: + continue + # A ';' between the parameter list and the next '{' means this was a + # declaration (or a call), not a definition. + if ";" in text[m.end() : open_brace]: + continue + depth, i = 0, open_brace + n = len(text) + while i < n: + ch = text[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + break + i += 1 + body = text[open_brace : i + 1] + names = _param_names_from_list(text[m.end() : open_brace]) + self._bodies.setdefault((cls, method), []).append((body, text, names)) + + # Free functions, for the pure-forwarder hop only. + for m in self._freepat.finditer(text): + name = m.group(1) + if name in ("if", "for", "while", "switch", "return", "catch", "sizeof"): + continue + open_brace = text.find("{", m.end()) + if open_brace < 0 or ";" in text[m.end() : open_brace]: + continue + if "::" in text[m.start() : m.end()]: + continue # already captured as a member definition + depth, i = 0, open_brace + n = len(text) + while i < n: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + break + i += 1 + self._free.setdefault(name, []).append( + ( + text[open_brace : i + 1], + text, + _param_names_from_list(text[m.end() : open_brace]), + ) + ) + + def guards(self, cls: str, method: str, param: str, index: int, kind: str) -> bool: + """True when SOME definition of ``cls::method`` guards this parameter. + + ``param`` is the HEADER's spelling and ``index`` its position. A C++ + definition is free to rename its parameters (``dial(const std::string& + tag)`` in the header is ``dial(..., const std::string& tag_in, ...)`` in + the .cpp, because the body shadows it with the resolved ``tag``), so the + definition-side name is resolved BY POSITION and the header name is only + a fallback. Matching on the header name alone silently found no guard for + every renamed parameter — and a missing guard reads as "the port really + sends this", i.e. a fold refused for a bookkeeping reason. + + Direct: the guard names the parameter inside the method body. + One transitive hop: the parameter is stored (the body mentions it) and a + guard on the corresponding MEMBER name appears elsewhere in the same + source file — the store-then-guard-at-serialization shape. NOT available + to the ``""`` sentinel: see the vocabulary note above (a stored-then- + serialization-guarded string is a real empty-string default, which is + exactly how both the reference and this port model ``pom::Section::body``). + """ + entries = self._bodies.get((cls, method)) + if not entries: + return False + patterns = _GUARD_PATTERNS.get(kind, []) + for body, file_text, names in entries: + local = names[index] if 0 <= index < len(names) else None + candidates = [n for n in (local, param) if n] + for name in candidates: + for pat in patterns: + if re.search(pat.format(n=re.escape(name)), body): + return True + # A LOCAL ALIAS still counts as a direct guard. C++ cannot + # reassign a ``const T&`` parameter, so the "resolve the sentinel + # to the real value" idiom has to copy first: + # std::string id = call_id; + # if (id.empty()) { id = ; } + # That is the same absence check as ``if (!call_id.empty())``, + # just one named local away, and it is still INSIDE this method — + # unlike the member/serialization hop, which is what distinguishes + # a modelled absence from a real empty-string default. Only a + # local DECLARED FROM this parameter qualifies. + for alias in re.findall( + r"\b(?:auto|[A-Za-z_][\w:<>,\s*&]*?)\s+([A-Za-z_]\w*)\s*=\s*" + + re.escape(name) + + r"\s*;", + body, + ): + for pat in patterns: + if re.search(pat.format(n=re.escape(alias)), body): + return True + # PURE FORWARDER. A method whose ENTIRE body is a single + # ``return (...);`` delegates its contract wholesale; the guard + # lives in the callee. ``AgentBase::handle_serverless_request`` is + # exactly this — its one statement is + # ``return utils::handle_serverless_request(*this, event, context, mode);`` + # and the free function does ``mode.empty() ? get_execution_mode() : mode``. + # Restricted to a body with ONE statement so it can only ever mean + # "this method IS the callee", never "somewhere downstream something + # is guarded". The callee's parameter is located BY NAME in the + # forwarding call's argument list, so an argument the forwarder + # reorders or wraps does not silently match. + fwd = _pure_forward_target(body) + if fwd is not None: + callee, args = fwd + for name in candidates: + if name not in args: + continue + arg_index = args.index(name) + for cbody, _cfile, cnames in self._free.get(callee, []): + cname = ( + cnames[arg_index] if 0 <= arg_index < len(cnames) else None + ) + for pat in patterns: + if cname and re.search( + pat.format(n=re.escape(cname)), cbody + ): + return True + if kind == "string": + continue + # One transitive hop: the body must actually USE the parameter (under + # EITHER spelling), and the member it lands on must be guarded in this + # file. The member is named after the CONCEPT, so every candidate + # spelling is tried for the member lookup once the parameter is known + # to be used — the definition's local name (``Section::add_subsection`` + # spells ``bullets`` as ``bs``) is not the member's name. + if not any( + re.search(r"\b" + re.escape(n) + r"\b", body) for n in candidates + ): + continue + for name in candidates: + for member in _member_spellings(name): + if member == name: + # The bare-name hop would re-match the body itself; only + # accept it OUTSIDE the body. + outside = file_text.replace(body, "", 1) + else: + outside = file_text + for pat in patterns: + if re.search(pat.format(n=re.escape(member)), outside): + return True return False - return any(tok.spelling == "=" for tok in tokens) + + def bodies(self, cls: str, method: str) -> list[tuple[str, list[str]]]: + """``[(body_text, param_names)]`` for every definition of ``cls::method``. + + The raw material the options-carrier unfold needs: whether a method's + ``const json&`` parameter is SPREAD onto the wire frame (a bag standing + in for the reference's keyword params) or CONSUMED as one domain value. + Only the body and the definition-side parameter names are exposed; the + file text stays private to the guard scan. + """ + return [ + (body, names) + for body, _file_text, names in self._bodies.get((cls, method), []) + ] # --------------------------------------------------------------------------- @@ -670,11 +1350,40 @@ def _has_default_value(arg) -> bool: # --------------------------------------------------------------------------- +def _merge_overload_optionality(a: dict, b: dict) -> None: + """Union ``required``/``default`` across two equal-arity overloads, in place. + + Positional match, because C++ overloads of one name share their parameter + ORDER (that is what makes them overloads). Only parameters whose optionality + DISAGREES are touched, and only in the permissive direction: if either side + declares a default, the caller can omit the argument, so both sides become + ``required: false`` carrying that default. Types and kinds are untouched — + those still come from whichever overload dedup selects. + + Arity mismatch means the two are not the same call shape (a convenience + wrapper, not a typed sibling); leave them alone. + """ + pa, pb = a.get("params", []), b.get("params", []) + if len(pa) != len(pb): + return + for x, y in zip(pa, pb, strict=False): + if x.get("kind") == "self" or y.get("kind") == "self": + continue + x_opt = x.get("required") is False + y_opt = y.get("required") is False + if x_opt == y_opt: + continue + src, dst = (x, y) if x_opt else (y, x) + dst["required"] = False + dst["default"] = src.get("default") + + def collect( raw_entries: list[dict], aliases: dict, raw_free_functions: list[dict] | None = None, raw_options_structs: dict[str, list[dict]] | None = None, + guards: GuardIndex | None = None, ) -> tuple[dict, list]: out_modules: dict = {} failures: list = [] @@ -699,7 +1408,8 @@ def collect( fctx = f"construction.{ref}.{f['name']}" try: ftype = _translate_with_canonical_fallback( - f.get("type", ""), f.get("canonical_type", ""), aliases, fctx) + f.get("type", ""), f.get("canonical_type", ""), aliases, fctx + ) except TypeTranslationError: ftype = "any" typed[f["name"]] = ftype or "any" @@ -716,9 +1426,12 @@ def collect( by_class[key]["methods"].extend(entry["methods"]) by_class[key]["fields"].extend(entry.get("fields") or []) else: - by_class[key] = {"namespace": ns, "name": name, - "methods": list(entry["methods"]), - "fields": list(entry.get("fields") or [])} + by_class[key] = { + "namespace": ns, + "name": name, + "methods": list(entry["methods"]), + "fields": list(entry.get("fields") or []), + } for (ns, name), entry in by_class.items(): # Check CLASS_RENAME_MAP first: (cpp_namespace, cpp_class) → @@ -749,15 +1462,44 @@ def collect( # Map C++ keyword-avoidance trailing underscore methods # (delete_, etc.) back to Python's unsuffixed names so the # diff lines up. - method_canonical = _METHOD_RENAMES.get(method_canonical, method_canonical) + method_canonical = _METHOD_RENAMES.get( + method_canonical, method_canonical + ) ctx = f"{mod}.{name}.{method_canonical}" try: - sig = build_signature(m, aliases, ctx) + sig = build_signature( + m, + aliases, + ctx, + guards=guards, + cpp_class=entry["name"], + cpp_method=(entry["name"] if native == "" else native), + ) except TypeTranslationError as e: failures.append(str(e)) continue if method_canonical in methods_out: existing = methods_out[method_canonical] + # OPTIONALITY IS A PROPERTY OF THE METHOD NAME, NOT OF ONE + # OVERLOAD. Only one overload survives dedup, but ``required`` + # asks a question about the CALLER: can they omit this argument? + # If ANY overload of the name defaults the parameter, they can. + # + # C++ forces this apart where a port ships a flat ``std::string`` + # overload alongside a typed ``enum class`` one: the string form + # defaults ``record_call(control_id="", stereo=false, + # format="wav", direction="both")``, but the typed form CANNOT + # repeat those defaults — two equal-arity overloads that are both + # callable with fewer arguments are ambiguous, so the compiler + # rejects it. The typed overload therefore declares them bare, + # and dedup (which prefers the typed form for the closed-set + # contract) was reporting ``required: true`` for four parameters + # a caller can plainly omit. + # + # Union optionality across equal-arity overloads before choosing + # a winner. Types/kinds still come from the chosen overload + # alone; only ``required``/``default`` merge. + _merge_overload_optionality(existing, sig) if ctx in PREFER_TYPED_OVERLOAD: # Equal-arity string-vs-enum overloads: keep the one that # TYPES more params (the enum-class form), so its closed-set @@ -770,8 +1512,9 @@ def collect( old_typed = _typed_param_count(existing) if new_typed < old_typed: continue - if new_typed == old_typed and \ - len(sig["params"]) >= len(existing["params"]): + if new_typed == old_typed and len(sig["params"]) >= len( + existing["params"] + ): continue elif ctx in PREFER_FULL_OVERLOAD: # Keep the LARGER-arity overload (the flat form that @@ -785,7 +1528,7 @@ def collect( continue methods_out[method_canonical] = sig - if not methods_out: + if not methods_out and not (entry.get("fields") or []): continue # Synthesize __init__ when libclang didn't surface an explicit # constructor — POD structs / classes with only the implicit @@ -807,11 +1550,13 @@ def collect( fctx = f"construction.{mod}.{name}.{f['name']}" try: ftype = _translate_with_canonical_fallback( - f.get("type", ""), f.get("canonical_type", ""), aliases, fctx) + f.get("type", ""), f.get("canonical_type", ""), aliases, fctx + ) except TypeTranslationError: ftype = "any" struct_fields.setdefault(f"{mod}.{name}", {}).setdefault( - f["name"], ftype or "any") + f["name"], ftype or "any" + ) out_modules.setdefault(mod, {"classes": {}}) out_modules[mod]["classes"].setdefault(name, {"methods": {}}) @@ -822,7 +1567,8 @@ def collect( # ctor param, so register it under its canonical class-ref too. if struct_fields.get(f"{mod}.{name}"): options_by_ref.setdefault( - f"class:{mod}.{name}", struct_fields[f"{mod}.{name}"]) + f"class:{mod}.{name}", struct_fields[f"{mod}.{name}"] + ) # Mixin projection — methods may live on AgentBase OR SWMLService # (Service is the parent class; many tool/auth/state helpers are @@ -839,8 +1585,16 @@ def collect( # methods are all defined on AgentBase and return ``AgentBase``; the # mixin class is just an interface marker. Don't retarget for those — # leaving the C++ AgentBase return matches Python's AgentBase return. - ab_entry = out_modules.get("signalwire.core.agent_base", {}).get("classes", {}).get("AgentBase") - svc_entry = out_modules.get("signalwire.core.swml_service", {}).get("classes", {}).get("SWMLService") + ab_entry = ( + out_modules.get("signalwire.core.agent_base", {}) + .get("classes", {}) + .get("AgentBase") + ) + svc_entry = ( + out_modules.get("signalwire.core.swml_service", {}) + .get("classes", {}) + .get("SWMLService") + ) if ab_entry or svc_entry: ab_methods = ab_entry["methods"] if ab_entry else {} svc_methods = svc_entry["methods"] if svc_entry else {} @@ -883,8 +1637,9 @@ def collect( # really produced the merged class. if retarget_returns: out_modules[target_mod]["classes"][target_cls]["methods"].setdefault( - "agent", {"params": [{"name": "self", "kind": "self"}], - "returns": "any"}) + "agent", + {"params": [{"name": "self", "kind": "self"}], "returns": "any"}, + ) projected.update(present) for n in projected: # ``__init__`` is COPIED to the synthetic projection targets @@ -988,7 +1743,10 @@ def collect( # ``HttpClient`` records request_options POSITIONAL on every transport verb, so # it is deliberately excluded. _RO_KEYWORD_BASE_CLASSES = { - "ReadResource", "CrudResource", "CrudWithAddresses", "FabricResource", + "ReadResource", + "CrudResource", + "CrudWithAddresses", + "FabricResource", } _bm = out_modules.get("signalwire.rest._base", {}) for _bcls_name, _bcls in _bm.get("classes", {}).items(): @@ -996,8 +1754,14 @@ def collect( continue for _bsig in _bcls.get("methods", {}).values(): _bparams = _bsig.get("params", []) - _ro_idx = next((i for i, p in enumerate(_bparams) - if p.get("name") == "request_options"), None) + _ro_idx = next( + ( + i + for i, p in enumerate(_bparams) + if p.get("name") == "request_options" + ), + None, + ) if _ro_idx is None: continue _bparams[_ro_idx]["kind"] = "keyword" @@ -1005,10 +1769,15 @@ def collect( # by libclang as a positional ``dict`` / var_keyword map), # so request_options lands at the reference's position and ``params`` is # the ignored trailing extra. - if _ro_idx == len(_bparams) - 1 and len(_bparams) >= 2 \ - and _bparams[_ro_idx - 1].get("name") == "params": - _bparams[_ro_idx - 1], _bparams[_ro_idx] = \ - _bparams[_ro_idx], _bparams[_ro_idx - 1] + if ( + _ro_idx == len(_bparams) - 1 + and len(_bparams) >= 2 + and _bparams[_ro_idx - 1].get("name") == "params" + ): + _bparams[_ro_idx - 1], _bparams[_ro_idx] = ( + _bparams[_ro_idx], + _bparams[_ro_idx - 1], + ) # Python-shape projection: when the Python reference uses ``**kwargs`` # (kind=var_keyword) for a method's last param, and the C++ port has a @@ -1023,6 +1792,13 @@ def collect( # contract is identical. _project_kwargs_shape(out_modules) + # METHOD-LEVEL options-carrier unfold. The construction contract already + # unfolds an options STRUCT param into its named fields (RelayClient( + # RelayConfig{...}) vs five reference kwargs); ordinary METHODS carry the + # same idiom and needed the same fold. Two carrier spellings, one rule — + # see _project_options_carrier for the evidence gates. + _project_options_carrier(out_modules, options_by_ref, guards) + # Callable-shape projection: when the Python reference uses a fully # parameterized ``callable,ret>`` annotation but the C++ # port emits a bare ``class:Callable`` (because the C++ side uses an @@ -1076,6 +1852,20 @@ def collect( # cpp_unified_action idiom, tracked in PORT_SIGNATURE_OMISSIONS). _project_relay_action_subclasses(out_modules) + # Built-in-skill value accessors: the skill classes live in + # ``src/skills/builtin/*.cpp``, which the libclang HEADER walk never opens, + # so an implemented accessor would otherwise read as missing-port drift. + # Signature-side twin of enumerate_surface's ``_project_builtin_skills``. + _project_skill_accessors(out_modules) + + # Built-in-skill SkillBase HOOK surface, same reason: the concrete skill + # classes are .cpp-only, so libclang never sees the hooks they define or + # inherit. porting-sdk 8496c77 made all 18 skill modules visible to the + # signature oracle, which is contract this enumerator must project rather + # than excuse. Emits the C++ SkillBase's OWN walked signatures, so it must + # run AFTER the header walk has populated signalwire.core.skill_base. + _project_builtin_skill_hooks(out_modules) + # AI-Chat signature fold: the Python reference records the whole AI-Chat # surface in ONE module (signalwire.ai_chat.client) with kwargs-exploded # method params; the C++ port splits it across ai_chat_client.hpp / @@ -1110,16 +1900,20 @@ def collect( # (the structs inherit from RelayEvent, so the generated-payload parser can't # reach them — this handles the inheriting form). Field-vs-getter idiom, RULES §2. _project_named_struct_getters( - out_modules, "signalwire.relay.event", - PORT_ROOT / "include" / "signalwire" / "relay" / "typed_events.hpp") + out_modules, + "signalwire.relay.event", + PORT_ROOT / "include" / "signalwire" / "relay" / "typed_events.hpp", + ) # RequestOptions optional-field getters (timeout/retries/retry_on_status/ # retry_backoff), gated on the oracle's _request_options getter set. abort_signal # is a pointer field (documented cpp_field_not_property omission) and merge is a # real method libclang already emits — neither is touched here. _project_named_struct_getters( - out_modules, "signalwire.rest._request_options", - PORT_ROOT / "include" / "signalwire" / "rest" / "request_options.hpp") + out_modules, + "signalwire.rest._request_options", + PORT_ROOT / "include" / "signalwire" / "rest" / "request_options.hpp", + ) sorted_modules = {} for k in sorted(out_modules): @@ -1141,7 +1935,8 @@ def collect( "generated_from": "signalwire-cpp via libclang", "modules": sorted_modules, "construction": build_construction( - sorted_modules, struct_fields, options_by_ref), + sorted_modules, struct_fields, options_by_ref + ), }, failures @@ -1150,10 +1945,22 @@ def collect( # --------------------------------------------------------------------------- # Members that are construction MECHANISM, never a construction parameter. -_CONSTRUCTION_NON_PARAMS = frozenset({ - "__init__", "__repr__", "__eq__", "from_payload", "from_params", "from_env", - "from_json", "to_json", "merge", "build", "builder", "clone", -}) +_CONSTRUCTION_NON_PARAMS = frozenset( + { + "__init__", + "__repr__", + "__eq__", + "from_payload", + "from_params", + "from_env", + "from_json", + "to_json", + "merge", + "build", + "builder", + "clone", + } +) # C++ ctor / accessor parameter spellings that name the SAME configurable as the # reference, under a different word. A RENAME (ALLOWLIST_DISCIPLINE §7 / RULES §2 @@ -1178,8 +1985,9 @@ def collect( _TRANSPORT_PARAM_RENAME = ("client", "http") -def _construction_params_from_signature(sig: dict, options_by_ref: dict, - ref_param_names: set) -> dict: +def _construction_params_from_signature( + sig: dict, options_by_ref: dict, ref_param_names: set +) -> dict: """Name-keyed construction params from an emitted ``__init__`` signature. A parameter whose TYPE is a known options struct is UNFOLDED into that @@ -1219,8 +2027,9 @@ def _construction_params_from_signature(sig: dict, options_by_ref: dict, return params -def build_construction(modules: dict, struct_fields: dict, - options_by_ref: dict | None = None) -> dict: +def build_construction( + modules: dict, struct_fields: dict, options_by_ref: dict | None = None +) -> dict: """Return ``{"module.Class": {"params": {name: {type, required}}}}``. A NAME-KEYED, unordered SET of configurable parameters — order, arity and @@ -1267,10 +2076,10 @@ def build_construction(modules: dict, struct_fields: dict, init = cinfo.get("methods", {}).get("__init__") if not isinstance(init, dict): continue - ref_names = set( - ref_construction.get(f"{mod}.{cls}", {}).get("params", {})) + ref_names = set(ref_construction.get(f"{mod}.{cls}", {}).get("params", {})) params = _construction_params_from_signature( - init, options_by_ref, ref_names) + init, options_by_ref, ref_names + ) if params: out[f"{mod}.{cls}"] = {"params": params} @@ -1323,8 +2132,11 @@ def build_construction(modules: dict, struct_fields: dict, continue if not isinstance(msig, dict): continue - args = [p for p in msig.get("params", []) - if (p.get("kind") or "positional") not in ("self", "cls")] + args = [ + p + for p in msig.get("params", []) + if (p.get("kind") or "positional") not in ("self", "cls") + ] if len(args) != 1: continue pname = mname[4:] @@ -1347,8 +2159,11 @@ def build_construction(modules: dict, struct_fields: dict, ref_names = set(ref_construction.get(key, {}).get("params", {})) params: dict = {} for pname, spec in entry["params"].items(): - if (pname == cpp_transport and ref_transport in ref_names - and cpp_transport not in ref_names): + if ( + pname == cpp_transport + and ref_transport in ref_names + and cpp_transport not in ref_names + ): pname = ref_transport pname = _CONSTRUCTION_PARAM_RENAMES.get(f"{key}.{pname}", pname) # A rename may collide with an already-canonical name; the ctor @@ -1361,8 +2176,15 @@ def build_construction(modules: dict, struct_fields: dict, def _load_rest_sidecar() -> dict: """Load the generator's rest_signatures.json (Class::method -> [records]).""" - sc = (PORT_ROOT / "include" / "signalwire" / "rest" / "namespaces" - / "generated" / "rest_signatures.json") + sc = ( + PORT_ROOT + / "include" + / "signalwire" + / "rest" + / "namespaces" + / "generated" + / "rest_signatures.json" + ) if not sc.is_file(): return {} return json.loads(sc.read_text()).get("methods", {}) @@ -1371,8 +2193,15 @@ def _load_rest_sidecar() -> dict: def _generated_class_modules() -> dict[str, str]: """Map each generated resource/container CLASS -> its python module, from generated_surface_map.json (the same source enumerate_surface projects).""" - smap = (PORT_ROOT / "include" / "signalwire" / "rest" / "namespaces" - / "generated" / "generated_surface_map.json") + smap = ( + PORT_ROOT + / "include" + / "signalwire" + / "rest" + / "namespaces" + / "generated" + / "generated_surface_map.json" + ) if not smap.is_file(): return {} return json.loads(smap.read_text()) @@ -1382,10 +2211,10 @@ def _generated_container_members() -> dict[str, list[str]]: """Parse the generated namespace-container headers for their public resource member fields (FabricNamespace { AiAgents ai_agents; ... }) so the client tree's accessor surface can be projected onto the oracle shape.""" - gen_dir = (PORT_ROOT / "include" / "signalwire" / "rest" / "namespaces" - / "generated") + gen_dir = PORT_ROOT / "include" / "signalwire" / "rest" / "namespaces" / "generated" out: dict[str, list[str]] = {} import re as _re + for hdr in gen_dir.glob("*Namespace.hpp"): src = hdr.read_text() m = _re.search(r"class (\w+Namespace)\s*\{(.*?)\n\};", src, _re.S) @@ -1433,19 +2262,24 @@ def _apply_rest_sidecar(out_modules: dict) -> None: # map; a miss means the map is stale — fail loud rather than drift. raise SystemExit( f"enumerate_signatures: sidecar class {cls!r} not in " - f"generated_surface_map.json (regenerate the REST layer)") + f"generated_surface_map.json (regenerate the REST layer)" + ) out_modules.setdefault(mod, {"classes": {}}) cls_entry = out_modules[mod]["classes"].setdefault(cls, {"methods": {}}) for canon, records in methods.items(): cls_entry["methods"][canon] = { - "params": [{"name": "self", "kind": "self"}] + [dict(r) for r in records], + "params": [{"name": "self", "kind": "self"}] + + [dict(r) for r in records], "returns": "any", } # Ensure a constructor is present (POD resource: implicit default ctor). - cls_entry["methods"].setdefault("__init__", { - "params": [{"name": "self", "kind": "self"}], - "returns": "void", - }) + cls_entry["methods"].setdefault( + "__init__", + { + "params": [{"name": "self", "kind": "self"}], + "returns": "void", + }, + ) # Client-tree container accessors: the Python oracle records each namespace # container's resource members (FabricNamespace.ai_agents, ...) as zero-arg @@ -1458,19 +2292,28 @@ def _apply_rest_sidecar(out_modules: dict) -> None: if mod is None: raise SystemExit( f"enumerate_signatures: container {cls!r} not in " - f"generated_surface_map.json (regenerate the REST layer)") + f"generated_surface_map.json (regenerate the REST layer)" + ) out_modules.setdefault(mod, {"classes": {}}) cls_entry = out_modules[mod]["classes"].setdefault(cls, {"methods": {}}) for member in members: - cls_entry["methods"].setdefault(member, { - "params": [{"name": "self", "kind": "self"}], - "returns": "any", - }) - cls_entry["methods"].setdefault("__init__", { - "params": [{"name": "self", "kind": "self"}, - {"name": "http", "type": "any", "required": True}], - "returns": "void", - }) + cls_entry["methods"].setdefault( + member, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "any", + }, + ) + cls_entry["methods"].setdefault( + "__init__", + { + "params": [ + {"name": "self", "kind": "self"}, + {"name": "http", "type": "any", "required": True}, + ], + "returns": "void", + }, + ) # A public data-member declaration inside one of the generated payload structs, @@ -1493,6 +2336,7 @@ def _gen_payload_ns_to_module() -> dict[str, str]: ``include/``. Import (don't hardcode) so a new payload namespace registered for the surface side is automatically covered here too.""" from enumerate_surface import GENERATED_PAYLOAD_NS # type: ignore + return dict(GENERATED_PAYLOAD_NS) @@ -1534,7 +2378,8 @@ def _gen_payload_struct_fields(payload_dir: Path) -> dict[str, list[str]]: # it inherits (``: public RelayEvent``), which the generated-payload ``struct Name {`` # regex above does not. Used for the relay Event dataclasses + RequestOptions. _NAMED_STRUCT_RE_SIG = re.compile( - r"(?:struct|class)\s+(\w+)\s*(?::[^{]+)?\{(.*?)\n\};", re.S) + r"(?:struct|class)\s+(\w+)\s*(?::[^{]+)?\{(.*?)\n\};", re.S +) def _named_struct_public_fields(header: Path) -> dict[str, list[str]]: @@ -1583,8 +2428,9 @@ def _oracle_class_members(module: str, cls: str) -> set[str]: out: set[str] = set(ref_cls.get("methods", {})) if ref_cls else set() if module == "signalwire.core.agent_base": for mod, entry in ref_modules.items(): - if mod != "signalwire.core.agent_base" and \ - not mod.startswith("signalwire.core.mixins."): + if mod != "signalwire.core.agent_base" and not mod.startswith( + "signalwire.core.mixins." + ): continue for cls_entry in entry.get("classes", {}).values(): out |= set(cls_entry.get("methods", {})) @@ -1613,10 +2459,13 @@ def _project_public_fields_as_getters(out_modules: dict, struct_fields: dict) -> continue for field in fields: if field in allowed: - cls_entry["methods"].setdefault(field, { - "params": [{"name": "self", "kind": "self"}], - "returns": "any", - }) + cls_entry["methods"].setdefault( + field, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "any", + }, + ) def _fold_setter_signatures(out_modules: dict) -> None: @@ -1648,10 +2497,13 @@ def _fold_setter_signatures(out_modules: dict) -> None: # shape — carrying the setter's ``(self, value) -> Self`` # signature over would be a spurious arity/return mismatch # against an attribute. - methods.setdefault(target, { - "params": [{"name": "self", "kind": "self"}], - "returns": "any", - }) + methods.setdefault( + target, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "any", + }, + ) def _project_named_struct_getters(out_modules: dict, module: str, header: Path) -> None: @@ -1678,18 +2530,19 @@ def _project_named_struct_getters(out_modules: dict, module: str, header: Path) ref_cls = ref_classes.get(cls) if not ref_cls: continue - oracle_getters = { - m for m in ref_cls.get("methods", {}) if m != "__init__" - } + oracle_getters = {m for m in ref_cls.get("methods", {}) if m != "__init__"} present = [f for f in fields if f in oracle_getters] if not present: continue cls_entry = mod_entry["classes"].setdefault(cls, {"methods": {}}) for field in present: - cls_entry["methods"].setdefault(field, { - "params": [{"name": "self", "kind": "self"}], - "returns": "any", - }) + cls_entry["methods"].setdefault( + field, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "any", + }, + ) # Oracle-recorded control methods per concrete RELAY call-action (mirrors @@ -1719,6 +2572,7 @@ def _project_ai_chat_signatures(out_modules: dict) -> None: symbols (abort-loud on a missing one) so nothing is invented. Drops the mis-routed native ai_chat modules.""" import re as _re + client_hpp = PORT_ROOT / "include" / "signalwire" / "ai_chat" / "ai_chat_client.hpp" if not client_hpp.is_file(): return # port doesn't ship AI-Chat @@ -1729,7 +2583,8 @@ def _need(pattern: str, what: str) -> None: raise SystemExit( f"enumerate_signatures: AI-Chat projection expected {what} in " f"{client_hpp.name} but it is gone -- fix the projection, do not " - f"emit a member the port no longer has") + f"emit a member the port no longer has" + ) # Client + RAII/verbs the projection reconciles must genuinely exist. _need(r"\bclass\s+AIChatClient\b", "class AIChatClient") @@ -1742,11 +2597,18 @@ def _need(pattern: str, what: str) -> None: # The class-B2 ctor-param reads the projection emits below. _need(r"\burl\s*\(\s*\)\s*const", "AIChatClient::url (reference self.url)") _need(r"\bint\s+code\s*\(\s*\)\s*const", "AIChatError::code") - _need(r"\bserver_message\s*\(\s*\)\s*const", - "AIChatError::server_message (reference message)") + _need( + r"\bserver_message\s*\(\s*\)\s*const", + "AIChatError::server_message (reference message)", + ) # Options structs whose fields the unfold below relies on. - for _s in ("AIChatClientOptions", "CreateConversationOptions", "ChatOptions", - "SummarizeOptions", "ConversationTurnOptions"): + for _s in ( + "AIChatClientOptions", + "CreateConversationOptions", + "ChatOptions", + "SummarizeOptions", + "ConversationTurnOptions", + ): _need(rf"\bstruct\s+{_s}\b", f"struct {_s}") # Error family + result structs. _need(r"\bclass\s+AIChatError\b", "class AIChatError") @@ -1773,32 +2635,38 @@ def _p(name, typ, required, default=None): # idiom divergence (cpp-stateless-transport), NOT invented here. aic = { "__init__": { - "params": [_self(), - _p("project", "optional", False), - _p("token", "optional", False), - _p("space", "optional", False), - _p("url", "optional", False)], + "params": [ + _self(), + _p("project", "optional", False), + _p("token", "optional", False), + _p("space", "optional", False), + _p("url", "optional", False), + ], "returns": "void", }, "chat": { - "params": [_self(), - _p("conversation_id", "string", True), - _p("message", "string", True), - _p("role", "string", False, "user"), - _p("config_url", "optional", False), - _p("user_metadata", "optional>", False), - _p("timeout", "optional", False), - _p("reinit", "bool", False, False)], + "params": [ + _self(), + _p("conversation_id", "string", True), + _p("message", "string", True), + _p("role", "string", False, "user"), + _p("config_url", "optional", False), + _p("user_metadata", "optional>", False), + _p("timeout", "optional", False), + _p("reinit", "bool", False, False), + ], "returns": "class:signalwire.ai_chat.client.ChatResponse", }, "create_conversation": { - "params": [_self(), - _p("conversation_id", "string", True), - _p("config_url", "string", True), - _p("user_message", "optional", False), - _p("timeout", "optional", False), - _p("user_metadata", "optional>", False), - _p("reinit", "bool", False, False)], + "params": [ + _self(), + _p("conversation_id", "string", True), + _p("config_url", "string", True), + _p("user_message", "optional", False), + _p("timeout", "optional", False), + _p("user_metadata", "optional>", False), + _p("reinit", "bool", False, False), + ], "returns": "class:signalwire.ai_chat.client.ConversationInfo", }, "end": { @@ -1814,8 +2682,11 @@ def _p(name, typ, required, default=None): "returns": "class:signalwire.ai_chat.client.ChatLog", }, "summarize": { - "params": [_self(), _p("conversation_id", "string", True), - _p("summary_prompt", "optional", False)], + "params": [ + _self(), + _p("conversation_id", "string", True), + _p("summary_prompt", "optional", False), + ], "returns": "string", }, # close() is a genuine C++ method (RAII no-op) folded onto the reference @@ -1831,9 +2702,11 @@ def _p(name, typ, required, default=None): err = { "__init__": { - "params": [_self(), - _p("code", "optional", True), - _p("message", "string", True)], + "params": [ + _self(), + _p("code", "optional", True), + _p("message", "string", True), + ], "returns": "void", }, # code / message: ctor params the reference stores publicly, recorded by @@ -1843,6 +2716,7 @@ def _p(name, typ, required, default=None): "code": {"params": [{"name": "self", "kind": "self"}], "returns": "any"}, "message": {"params": [{"name": "self", "kind": "self"}], "returns": "any"}, } + # Each result DTO is @dataclass-shaped in the reference: besides ``__init__`` # the oracle records every field as a zero-arg property getter. The C++ port # carries them as public struct fields; emit the oracle's getter shape (self- @@ -1853,10 +2727,12 @@ def _getter() -> dict: conv_info = { "__init__": { - "params": [_self(), - _p("id", "string", True), - _p("status", "string", True), - _p("initial_message", "optional", False)], + "params": [ + _self(), + _p("id", "string", True), + _p("status", "string", True), + _p("initial_message", "optional", False), + ], "returns": "void", }, "id": _getter(), @@ -1865,10 +2741,12 @@ def _getter() -> dict: } chat_resp = { "__init__": { - "params": [_self(), - _p("text", "string", True), - _p("conversation_id", "string", True), - _p("user_event", "optional>", False)], + "params": [ + _self(), + _p("text", "string", True), + _p("conversation_id", "string", True), + _p("user_event", "optional>", False), + ], "returns": "void", }, "text": _getter(), @@ -1877,9 +2755,11 @@ def _getter() -> dict: } chat_log = { "__init__": { - "params": [_self(), - _p("messages", "list>", False, "list()"), - _p("call_timeline", "list>", False, "list()")], + "params": [ + _self(), + _p("messages", "list>", False, "list()"), + _p("call_timeline", "list>", False, "list()"), + ], "returns": "void", }, "messages": _getter(), @@ -1887,8 +2767,10 @@ def _getter() -> dict: } # Drop the mis-routed native modules, emit the single canonical one. - for _native in ("signalwire.ai_chat.ai_chat_client", - "signalwire.ai_chat.ai_chat_error"): + for _native in ( + "signalwire.ai_chat.ai_chat_client", + "signalwire.ai_chat.ai_chat_error", + ): out_modules.pop(_native, None) mod = out_modules.setdefault("signalwire.ai_chat.client", {"classes": {}}) @@ -1920,17 +2802,29 @@ def _project_relay_action_subclasses(out_modules: dict) -> None: ) if not action_cls: return - call_mod = out_modules.setdefault("signalwire.relay.call", {"classes": {}, "functions": {}}) + call_mod = out_modules.setdefault( + "signalwire.relay.call", {"classes": {}, "functions": {}} + ) call_classes = call_mod.setdefault("classes", {}) # The BASE ``Action``: the reference declares it in ``signalwire.relay.call`` - # with __init__/is_done/wait/result plus the two ctor params it stores - # publicly — ``call`` (the back-reference) and ``control_id`` — which the - # oracle's class-B2 rule records. The unified C++ Action carries all of - # them; project the reference-recorded subset onto relay.call so the base + # with __init__/is_done/wait/result plus the ctor/derived attrs it stores + # publicly — ``call`` (the back-reference), ``control_id``, and + # ``completed`` (the bool state flag that starts False and flips True on + # completion) — which the oracle's class-B2 rule records. The unified C++ + # Action carries all of them (``is_done()`` delegates to ``completed()``); + # project the reference-recorded subset onto relay.call so the base # symbol lines up (the richer C++ surface stays under relay.action). base_entry = call_classes.setdefault("Action", {"methods": {}}) - for m in ("__init__", "is_done", "wait", "result", "control_id", "call"): + for m in ( + "__init__", + "is_done", + "wait", + "result", + "control_id", + "call", + "completed", + ): if m in action_cls: base_entry["methods"].setdefault(m, action_cls[m]) # ``call`` is REFERENCE surface (relay.call.Action.call), now homed on the @@ -1946,6 +2840,368 @@ def _project_relay_action_subclasses(out_modules: dict) -> None: entry["methods"][m] = action_cls[m] +# Built-in-skill members the oracle records as a SIGNATURE (not merely surface +# membership) and that the C++ port implements as a zero-arg accessor on the +# skill class. The skill classes live in ``.cpp`` IMPLEMENTATION files the +# header walker never opens — so libclang cannot see them and the accessor +# would read as missing-port drift even though it is implemented. This is the +# signature-side twin of enumerate_surface's ``_project_builtin_skills``: same +# fold, same fail-honest rule. +# +# ``oracle_key -> (candidate cpp sources, cpp class, {member: accessor})``. +# The tuple of sources used to carry TWO entries for spider, because the skill +# was registered twice — once by ``src/skills/builtin/spider.cpp`` and once by a +# duplicate ``SpiderSkillR`` in ``src/skills/skill_registry.cpp`` — with +# ``register_skill`` silently overwriting, so which class ran was decided by +# unspecified cross-TU static-init order. That duplication is now GONE (the +# ``*SkillR`` copies were deleted and ``register_skill`` throws on a duplicate +# name), so each skill has exactly one defining source and the file the +# enumerator reads is the file that runs. +# +# A member is projected ONLY when the named accessor is genuinely present in +# EVERY listed source — a deleted or renamed accessor drops out rather than +# being invented (RULES §2/§3). +_SKILL_ACCESSOR_PROJECTIONS: dict[str, tuple[tuple[str, ...], str, dict[str, str]]] = { + "signalwire.skills.spider.skill.SpiderSkill": ( + ("src/skills/builtin/spider.cpp",), + "SpiderSkill", + # ``self.remove_xpaths`` — the PREFILLED xpath list the reference sets + # in ``__init__`` and walks in ``_fast_text_extract``. C++ idiom: a + # field plus a ``remove_xpaths()`` reader (+ ``set_remove_xpaths``). + {"remove_xpaths": "remove_xpaths"}, + ), +} + + +def _project_skill_accessors(out_modules: dict) -> None: + """Project built-in-skill value accessors (see _SKILL_ACCESSOR_PROJECTIONS). + + The C++ skill classes are defined in ``.cpp`` implementation files, which + the libclang header walk never parses. Verify the accessor really exists in + every candidate source and emit the oracle-shaped zero-arg signature for + it; skip it entirely when any source or the accessor is absent, so the + enumerator can never invent surface the port does not have. + """ + for oracle_key, ( + cpp_files, + _cpp_cls, + members, + ) in _SKILL_ACCESSOR_PROJECTIONS.items(): + srcs = [PORT_ROOT / f for f in cpp_files] + if not all(s.is_file() for s in srcs): + continue # skill not implemented in this tree — don't invent it + texts = [s.read_text(encoding="utf-8") for s in srcs] + module, cls = oracle_key.rsplit(".", 1) + for member, accessor in members.items(): + # The accessor must be DEFINED (``name() const {`` / ``name() {``), + # not merely mentioned. A bare call site does not count. + pat = re.compile( + r"\b" + + re.escape(accessor) + + r"\s*\(\s*\)\s*(?:const\s*)?(?:noexcept\s*)?\{" + ) + if not all(pat.search(t) for t in texts): + continue + mod_entry = out_modules.setdefault(module, {"classes": {}, "functions": {}}) + cls_entry = mod_entry.setdefault("classes", {}).setdefault( + cls, {"methods": {}} + ) + cls_entry["methods"].setdefault( + member, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "list", + }, + ) + + +# --------------------------------------------------------------------------- +# Built-in-skill HOOK projection (the signature-side twin of +# enumerate_surface's ``_project_builtin_skills``) +# --------------------------------------------------------------------------- +# Until 2026-07-30 the SIGNATURE oracle recorded only 7 of the 18 builtin skill +# modules: ``enumerate_python_signatures`` dropped every base-identical override, +# which emptied a skill class's ``methods_out`` and deleted the CLASS — and with +# it the module. porting-sdk 8496c77 fixed that (class survival is now the +# invariant), so all 18 skill modules are visible and each skill's SkillBase hook +# set (``setup``/``register_tools``/``get_hints``/``get_global_data``/ +# ``get_prompt_sections``/``get_parameter_schema``/``get_instance_key``/ +# ``cleanup``) is recorded contract. +# +# This enumerator carried the matching stale assumption: it projected exactly ONE +# skill member (``SpiderSkill.remove_xpaths``, above) on the premise that the +# signature oracle recorded skill subclasses method-LESS. That premise is dead, +# so the hooks now project generally. +# +# THE HOOKS ARE REAL C++ SURFACE, NOT INVENTED. Every C++ builtin skill derives +# from ``signalwire::skills::SkillBase``, whose hooks are virtual with real +# bodies (``get_hints``/``get_global_data``/``get_prompt_sections``/ +# ``get_parameter_schema``/``get_instance_key``/``cleanup``) or pure-virtual and +# overridden in each skill (``setup``/``register_tools``). A caller holding a +# concrete skill can call all of them; libclang simply never sees the classes +# because they live in ``src/skills/builtin/*.cpp`` implementation files that the +# HEADER walk does not open. +# +# The emitted signature is the C++ ``SkillBase``'s OWN recorded signature for +# that hook — read back out of ``out_modules`` after the header walk, never +# hand-written — so the audit compares the port's genuine shape against the +# reference. Nothing is flattened to a convenient shape to go green: of the eight +# hooks, five (``get_hints``/``get_global_data``/``get_instance_key``/ +# ``cleanup``/``get_parameter_schema``) match the reference exactly and go +# straight to zero drift. +# +# Three hooks do NOT go to zero, and that is the correct outcome rather than a +# gap in this projection. ``setup`` (C++ takes the params json at attach time), +# ``register_tools`` and ``get_prompt_sections`` (C++ RETURNS the typed payload +# instead of calling back into the agent) genuinely diverge from the reference on +# ``SkillBase`` ITSELF — a real, pre-existing C++ design divergence already +# recorded once on the base in PORT_SIGNATURE_OMISSIONS +# (``cpp_typed_overload_subset`` / ``cpp_typed_skill_pipeline``). PHP, which +# absorbed the same oracle change with zero findings, implements the reference +# shapes literally (``setup(self) -> bool``, ``register_tools() -> void``), so +# this is C++'s divergence and not an artifact of the audit. +# +# They are projected ANYWAY, with their true C++ shape, so the audit reports what +# actually diverges (``param-count-mismatch`` / ``return-mismatch``) rather than +# the false ``missing-port`` that withholding them would produce — the methods +# are real, present and callable; only their shape differs. +# +# Fail-honest, four ways: +# 1. the skill's ``.cpp`` must exist and define the C++ class (the shared +# ``enumerate_surface._scan_skill_methods`` scan, same source of truth as +# the surface projection); +# 2. the member must be one the C++ class genuinely has — own-defined or a +# real ``SkillBase`` hook it inherits; +# 3. the reference oracle must record that member on that class (read LIVE from +# python_signatures.json, never from a hand-maintained list — the surface +# enumerator's ``py_methods`` lists are the SURFACE oracle's and are wider +# than the signature oracle's, so trusting them emitted four skills' +# hooks the signature reference does not record); +# 4. ``SkillBase`` must genuinely carry that hook in the walked headers. +# A member failing any of the four is dropped, never invented. +# +# --------------------------------------------------------------------------- +# INHERITED-DIVERGENCE FOLD (the per-subclass repetition of a base-level fact) +# --------------------------------------------------------------------------- +# Projecting the base's true C++ shape onto all 18 subclasses made the audit +# report the SAME THREE divergences 31 more times — once per concrete skill that +# inherits ``setup`` / ``register_tools`` / ``get_prompt_sections``. Those three +# are one C++ design decision taken ONCE on ``SkillBase`` and already described +# exactly once in PORT_SIGNATURE_OMISSIONS (``cpp_typed_overload_subset`` / +# ``cpp_typed_skill_pipeline``). A subclass that merely inherits it is not a +# second divergence; re-reporting it per-subclass is the audit counting one fact +# 31 times, which is idiom repetition, and idiom folds at the emitter (RULES §2). +# +# So: when a concrete skill's override is SIGNATURE-IDENTICAL to the hook +# ``SkillBase`` declares, the subclass adds no divergence of its own, and the +# projected member is emitted in the REFERENCE's shape — the base keeps the one +# recorded description of the difference, and the subclass compares equal. +# +# THE FOLD CANNOT HIDE A GENUINE MEMBER. It is gated on the C++ SOURCE, not on a +# hand-kept list: ``_scan_skill_hook_decls`` reads each skill's own ``.cpp`` and +# ``SkillBase``'s header and compares the NORMALIZED DECLARATION TEXT (return +# type, parameter list, const-qualifier). A skill whose override diverges from +# the base in ANY of those — a different return type, an extra/renamed/retyped +# parameter, a dropped ``const`` — does NOT match, is emitted with the true C++ +# ``SkillBase``-walked signature instead, and surfaces as drift exactly as it +# does today. Likewise a hook the base does not declare, or a skill whose source +# the scan cannot read, never folds. (Negative control: perturbing one skill's +# override in the source re-reds the gate — see the commit message.) +# +# Only the base-divergent hooks are foldable. The five hooks that already match +# the reference (``get_hints`` / ``get_global_data`` / ``get_instance_key`` / +# ``cleanup`` / ``get_parameter_schema``) need no fold and are excluded, so the +# fold is scoped to the divergence it describes rather than blanketing the hook +# set. +_SKILL_BASE_ORACLE_KEY = "signalwire.core.skill_base" +_SKILL_BASE_CPP_CLASS = "SkillBase" +_SKILL_BASE_HEADER = "include/signalwire/skills/skill_base.hpp" + +# The hooks whose C++ shape diverges from the reference ON ``SkillBase`` ITSELF, +# recorded once each in PORT_SIGNATURE_OMISSIONS. These — and only these — are +# the members whose per-subclass repetition is folded. Keep this in step with +# the base entries: a hook listed here MUST be excused on ``SkillBase``, or the +# fold would be hiding a divergence nothing describes. ``_project_builtin_skill_hooks`` +# enforces that at runtime against PORT_SIGNATURE_OMISSIONS. +_SKILL_BASE_DIVERGENT_HOOKS = { + "setup", # cpp_typed_overload_subset: C++ takes the params json + "register_tools", # cpp_typed_skill_pipeline: C++ returns the typed payload + "get_prompt_sections", # cpp_typed_skill_pipeline: ditto +} + + +def _normalize_decl(text: str) -> str: + """Collapse a C++ declarator to comparable text (whitespace-insensitive).""" + return re.sub(r"\s+", " ", text).strip() + + +def _scan_skill_hook_decls( + repo: Path, +) -> tuple[dict[str, str], dict[str, dict[str, str]]]: + """Read the C++ SOURCE and return the normalized declaration text for each + base hook, on ``SkillBase`` and on every builtin skill that overrides it. + + Returns ``(base_decls, per_skill_decls)`` where ``base_decls`` maps + ``hook -> " ()[ const]"`` for ``SkillBase``, and + ``per_skill_decls`` maps ``cpp_class -> {hook: same-shaped text}``. A hook a + skill does not override is simply absent (it inherits, so its shape IS the + base's). + + This is the fold's gate: a subclass folds only when its text equals the + base's. Anything the scan cannot read yields no entry and therefore no fold. + """ + hooks = "|".join(sorted(_SKILL_BASE_DIVERGENT_HOOKS)) + # `` () [const] [override] {`` or ``... = 0;`` + decl_re = re.compile( + r"(?:\[\[nodiscard\]\]\s*)?(?:virtual\s+)?" + r"([A-Za-z_][\w:<>,\s*&]*?)\s+" + r"\b(" + hooks + r")\s*" + r"\(([^;{]*?)\)\s*" + r"(const\b)?\s*" + r"(?:override\b\s*)?(?:noexcept\b\s*)?(?:=\s*0\s*;|\{)" + ) + + def _decls(text: str) -> tuple[dict[str, str], str]: + text = strip_block_comments(text) + text = "\n".join(strip_line_comments(ln) for ln in text.splitlines()) + out: dict[str, str] = {} + for ret, name, params, is_const in decl_re.findall(text): + out.setdefault( + name, + _normalize_decl(f"{ret} {name}({params}) {is_const}"), + ) + return out, text + + base_path = repo / _SKILL_BASE_HEADER + base_decls: dict[str, str] = {} + if base_path.is_file(): + base_decls, _ = _decls(base_path.read_text(encoding="utf-8")) + + per_skill: dict[str, dict[str, str]] = {} + src = repo / SKILL_SOURCE_DIR + if src.is_dir(): + class_re = re.compile(r"\bclass\s+([A-Za-z_]\w*Skill)\b") + for cpp in sorted(src.glob("*.cpp")): + decls, stripped = _decls(cpp.read_text(encoding="utf-8")) + for cls in class_re.findall(stripped): + per_skill.setdefault(cls, {}).update(decls) + return base_decls, per_skill + + +def _skill_base_divergences_are_recorded() -> set[str]: + """Return the subset of ``_SKILL_BASE_DIVERGENT_HOOKS`` that the port's + PORT_SIGNATURE_OMISSIONS genuinely excuses on ``SkillBase``. + + The fold's premise is that the divergence is described exactly ONCE, on the + base. If an entry is ever deleted, the fold must stop folding that hook — + otherwise the subclasses would silently absorb a difference nothing records. + Read the ledger, never assume it. + """ + doc = PORT_ROOT / "PORT_SIGNATURE_OMISSIONS.md" + if not doc.is_file(): + return set() + text = doc.read_text(encoding="utf-8") + recorded = set() + for hook in _SKILL_BASE_DIVERGENT_HOOKS: + if re.search( + r"^\s*signalwire\.core\.skill_base\.SkillBase\." + + re.escape(hook) + + r"\s*:", + text, + re.MULTILINE, + ): + recorded.add(hook) + return recorded + + +def _project_builtin_skill_hooks(out_modules: dict) -> None: + """Project each builtin skill's SkillBase hook surface into its Python- + canonical module, reusing the C++ ``SkillBase``'s own walked signatures — + except for the three hooks whose divergence is a BASE-level fact already + recorded once, where a signature-identical override folds to the reference + shape (see the block comment above).""" + try: + from enumerate_surface import ( # type: ignore + SKILL_PROJECTIONS, + _SKILL_BASE_METHODS, + _scan_skill_methods, + ) + except ImportError: # pragma: no cover - enumerate_surface is a hard sibling + return + + ref = _load_python_signatures() + ref_modules = ref.get("modules", {}) if ref else {} + if not ref_modules: + return # no oracle to gate against -- emit nothing rather than guess + + base_methods = ( + out_modules.get(_SKILL_BASE_ORACLE_KEY, {}) + .get("classes", {}) + .get(_SKILL_BASE_CPP_CLASS, {}) + .get("methods", {}) + ) + if not base_methods: + raise SystemExit( + "enumerate_signatures: builtin-skill hook projection found no walked " + f"{_SKILL_BASE_ORACLE_KEY}.{_SKILL_BASE_CPP_CLASS} signatures -- the " + "header walk changed; fix the projection rather than emitting a " + "hand-written shape" + ) + + base_decls, skill_decls = _scan_skill_hook_decls(PORT_ROOT) + if not base_decls: + raise SystemExit( + "enumerate_signatures: could not read the SkillBase hook declarations " + f"from {_SKILL_BASE_HEADER} -- the inherited-divergence fold is gated " + "on that source text; fix the scan rather than folding blind" + ) + foldable = _SKILL_BASE_DIVERGENT_HOOKS & _skill_base_divergences_are_recorded() + + defined = _scan_skill_methods(PORT_ROOT) + for cpp_cls, (module, py_cls, _py_methods) in SKILL_PROJECTIONS.items(): + if cpp_cls not in defined: + continue # skill not implemented in this tree -- don't invent it + ref_members = ( + ref_modules.get(module, {}) + .get("classes", {}) + .get(py_cls, {}) + .get("methods", {}) + ) + if not ref_members: + continue # the signature oracle records nothing here -- nothing to fold + own = defined[cpp_cls] + mod_entry = out_modules.setdefault(module, {"classes": {}, "functions": {}}) + cls_entry = mod_entry.setdefault("classes", {}).setdefault( + py_cls, {"methods": {}} + ) + for member in sorted(ref_members): + # ``__init__`` and skill-specific helpers (``get_tools`` / + # ``search_wiki`` / ``remove_xpaths``) are NOT projected here: the + # ctor is a per-skill construction shape, and the helpers are covered + # by the accessor projection above. + if member not in _SKILL_BASE_METHODS: + continue + # The C++ class must genuinely have it: own-defined, or inherited. + if member not in own and member not in _SKILL_BASE_METHODS: + continue + sig = base_methods.get(member) + if sig is None: + continue # SkillBase lost the hook -- drop, never invent + # INHERITED-DIVERGENCE FOLD. Only for a hook whose base-level + # divergence the ledger genuinely records, and only when this + # skill's own C++ declaration is byte-equal (modulo whitespace) to + # the one SkillBase declares. Anything else keeps the true C++ + # shape and stays visible to the audit. + if member in foldable: + base_text = base_decls.get(member) + own_text = skill_decls.get(cpp_cls, {}).get(member, base_text) + if base_text is not None and own_text == base_text: + ref_sig = ref_members.get(member) + if ref_sig is not None: + sig = ref_sig + cls_entry["methods"].setdefault(member, json.loads(json.dumps(sig))) + + def _project_gen_payload_getters(out_modules: dict) -> None: """Project the generated payload structs' data-member FIELDS as zero-arg property-getter methods so they match the Python oracle's getter shape, @@ -1960,15 +3216,44 @@ def _project_gen_payload_getters(out_modules: dict) -> None: enumerator force-registering these as method-less types (empty member list on both sides → SURFACE-DIFF green). - Only project a field whose wire-key name the oracle records as a getter for - that class. Port-only data members the reference does not expose as a - property (e.g. the open ``extras`` member, and the wire keys Python's - payload class simply doesn't surface) are NOT projected — projecting them - would invent method surface the reference lacks. Each getter is emitted with - the oracle's zero-arg shape and an ``any`` return (``types_compatible`` - treats ``any`` as compatible with the oracle's typed ``union<…>`` / - ``class:…`` getter returns), matching the container-accessor projection in - ``_apply_rest_sidecar``. + Project EVERY field these structs declare — deliberately NOT only the ones + the oracle happens to record. The field set is derived from the SPEC, not + mirrored from the reference: each of these headers is emitted by a + generator from its authoritative spec (``schema.json`` ``$defs`` for + swml_verbs, the swaig/post-prompt component schemas, the RELAY protocol + spec), every file is marked ``DO NOT EDIT``, and the GEN-FRESH/-SWML/ + -RELAY/-SWAIG gates byte-compare the committed tree to a fresh regen. So a + field is present here IF AND ONLY IF the spec declares it — which is a + STRONGER guarantee than "the oracle also lists it", and it is what makes + projecting the full set safe under RULES §3 (no invented surface). + + Intersecting with the oracle instead — ``[f for f in fields if f in + oracle_getters]`` — is what this function used to do, and it is a permanent + blind spot: it DEFINES the port's projected surface as a subset of the + reference's, so a field the port implements and the reference lacks can + never drift. Measured 2026-08-05 by negative control: adding a bogus + ``totally_invented_field`` to ``ai_params.hpp`` left the enumerated surface + completely unchanged, because the oracle does not record that name. It also + made the projection track the oracle silently — when the oracle widened + AIParams 60 → 87, this function's output followed it with no change to the + port and no gate ever verifying the port. Deleting a real field IS still + caught (it disappears from the port side and DRIFT reports it missing), so + the old rule failed in exactly one direction: the additive one. + + Keeping the per-CLASS oracle gate (``ref_cls`` below) is a different + question and is retained: a payload class the reference has no counterpart + for at all (the 123 ``relay.protocol_types_generated`` structs, whose + Python module does not exist) is left unprojected rather than emitted as + 123 classes of unmatchable surface. That is a module-scope decision, not a + field-level filter, and it does not hide drift WITHIN a class both sides + have. + + The open ``extras`` member is not a wire key and is excluded upstream by + the parser (it carries an initializer, not an ``std::optional`` wire + field). Each getter is emitted with the oracle's zero-arg shape and an + ``any`` return (``types_compatible`` treats ``any`` as compatible with the + oracle's typed ``union<…>`` / ``class:…`` getter returns), matching the + container-accessor projection in ``_apply_rest_sidecar``. """ ref = _load_python_signatures() if not ref: @@ -1989,23 +3274,25 @@ def _project_gen_payload_getters(out_modules: dict) -> None: ref_cls = ref_classes.get(cls) if not ref_cls: continue - oracle_getters = { - m for m in ref_cls.get("methods", {}) if m != "__init__" - } - present = [f for f in fields if f in oracle_getters] - if not present: + if not fields: continue cls_entry = mod_entry["classes"].setdefault(cls, {"methods": {}}) - for field in present: - cls_entry["methods"].setdefault(field, { - "params": [{"name": "self", "kind": "self"}], - "returns": "any", - }) + for field in fields: + cls_entry["methods"].setdefault( + field, + { + "params": [{"name": "self", "kind": "self"}], + "returns": "any", + }, + ) # Method-less POD: implicit default constructor is available. - cls_entry["methods"].setdefault("__init__", { - "params": [{"name": "self", "kind": "self"}], - "returns": "void", - }) + cls_entry["methods"].setdefault( + "__init__", + { + "params": [{"name": "self", "kind": "self"}], + "returns": "void", + }, + ) def _project_kwargs_shape(out_modules: dict) -> None: @@ -2066,6 +3353,281 @@ def project_one(port_sig: dict, ref_sig: dict) -> None: project_one(sig, ref_sig) +# --------------------------------------------------------------------------- +# Method-level options-carrier unfold +# --------------------------------------------------------------------------- +# +# THE IDIOM +# ========= +# Python spells "these N knobs are individually optional" as N keyword-only +# params. C++ has no keyword arguments, so the SAME contract is carried by ONE +# object parameter — either a typed options STRUCT (``RenderOptions``) or an +# untyped ``const json&`` bag. The construction contract already unfolds the +# struct form (``RelayClient(RelayConfig{project, token, …})`` vs five reference +# kwargs, see ``_construction_params_from_signature``); ordinary METHODS carry +# exactly the same idiom, and this is the same fold applied to them. +# +# Folding at the emitter is the point: the diff keeps comparing every named knob +# afterwards, so dropping or renaming one still reports. An omission would stop +# comparing the whole method — which is what the nine ``cpp_options_object`` / +# ``cpp_options_struct`` entries this replaces were doing. +# +# THE BOUNDARY — a carrier, not every dict-typed parameter +# ======================================================== +# A single dict-typed parameter is NOT automatically a carrier. The reference has +# genuine DOMAIN parameters whose value simply IS a dict — ``Call.execute_swml( +# swml)``, ``Call.refer(device)``, ``DataMap.foreach(mapping)``. Unfolding one of +# those would invent params the port never had and destroy a real one. The fold +# therefore fires only on EVIDENCE, never on the parameter's type alone: +# +# TYPED-STRUCT form — the param's emitted type resolves to a known options +# struct (``options_by_ref``). The struct's public FIELDS are the named set; +# they are declared, not guessed. Skipped when the REFERENCE also declares a +# param of that name (the reference passes the same object, so the carrier IS +# the contract — same exclusion the construction unfold makes for +# ``RestClient(request_options=…)``). +# +# UNTYPED-BAG form — the param is the untyped ``any`` (``const json&``) AND the +# method body SPREADS it onto the outgoing frame rather than reading fixed +# keys out of it. ``Call::queue_enter`` is ``json p = params.is_object() ? +# params : json::object(); p["queue_name"] = queue_name; return +# execute_simple("queue.enter", p);`` — every key the caller puts in the bag +# reaches the wire verbatim, so the reference's keyword names ARE reachable +# through it and the two calls produce identical frames. A body that instead +# does ``params["mapping"]`` / ``p["swml"] = swml`` is consuming a domain +# value and is left alone. +# +# In both forms the reference must have named keyword params to unfold TO, and +# the port must not already declare them. Anything unproven stays as it was and +# reports drift, which is the correct outcome for a case this cannot decide. +_BAG_SPREAD_RE = ( + # ``json p = .is_object() ? : json::object();`` — the copy-the + # whole-bag-then-add-fixed-keys idiom every Call.* action method uses. + r"=\s*{n}\s*\.\s*is_object\s*\(\s*\)\s*\?\s*{n}\b", + # ``for (auto& [k, v] : .items()) out[k] = v;`` — explicit spread. + r"\b{n}\s*\.\s*items\s*\(\s*\)", + # ``p.update()`` / ``p.merge_patch()`` / ``p.insert(…{n}…)``. + r"\.\s*(?:update|merge_patch)\s*\(\s*{n}\s*\)", +) + + +def _is_callable_type(t: str) -> bool: + """True for a canonical type whose value is a function, not JSON data. + + ``callable<…>`` and ``optional>`` are the two spellings the + oracle records. A JSON bag cannot hold either. + """ + t = (t or "").strip() + if t.startswith("optional<") and t.endswith(">"): + t = t[len("optional<") : -1].strip() + return t.startswith("callable<") or t == "callable" + + +def _bag_is_spread( + guards: GuardIndex | None, cls: str, method: str, param: str, index: int +) -> bool: + """True when SOME definition of ``cls::method`` spreads ``param`` wholesale. + + Same position-then-name resolution the guard scan uses: a definition may + rename its parameters relative to the header, so the definition-side name at + this index is tried first and the header spelling is the fallback. + """ + if guards is None: + return False + for body, names in guards.bodies(cls, method): + local = names[index] if 0 <= index < len(names) else None + for name in (n for n in (local, param) if n): + for pat in _BAG_SPREAD_RE: + if re.search(pat.format(n=re.escape(name)), body): + return True + return False + + +def _project_options_carrier( + out_modules: dict, options_by_ref: dict, guards: GuardIndex | None +) -> None: + """Unfold a method's options carrier into the reference's keyword params. + + See the block comment above for the idiom and the two evidence gates. The + unfolded params are emitted ``kind: keyword`` with the reference's own type + and default, because that is what the carrier genuinely offers: C++ aggregate + init and a JSON bag both let the caller set any subset, in any order. + """ + ref = _load_python_signatures() + if not ref: + return + for mod, entry in out_modules.items(): + ref_classes = ref.get("modules", {}).get(mod, {}).get("classes", {}) + if not ref_classes: + continue + for cls, cinfo in entry.get("classes", {}).items(): + ref_methods = ref_classes.get(cls, {}).get("methods", {}) + if not ref_methods: + continue + for meth, sig in cinfo.get("methods", {}).items(): + # ``__init__`` is the CONSTRUCTION contract's, not this fold's. + # ``build_construction`` runs its own unfold with deliberately + # different semantics: it emits the options struct's WHOLE field + # set, because a construction knob the reference lacks is still a + # real configurable the port offers (and the diff reports it as + # such). Folding here first would consume the carrier and hide + # those — measured: it silently dropped RelayConfig's ``port`` / + # ``max_connections`` / ``request_timeout_ms`` from + # ``RelayClient``'s construction params. + if meth == "__init__": + continue + ref_sig = ref_methods.get(meth) + if ref_sig: + _unfold_one_carrier(sig, ref_sig, options_by_ref, guards, cls, meth) + + +def _unfold_one_carrier( + sig: dict, + ref_sig: dict, + options_by_ref: dict, + guards: GuardIndex | None, + cls: str, + meth: str, +) -> None: + port_params = sig.get("params", []) + ref_params = ref_sig.get("params", []) + if not port_params or not ref_params: + return + # The reference's OPTIONAL named params are what a carrier stands in for. + # Two exclusions: + # * ``self``/``cls`` and the trailing ``**kwargs`` — the latter is already + # reconciled by ``_project_kwargs_shape``, and consuming it here would + # let a carrier match a method whose only reference knob is the open + # spread. + # * a REQUIRED param — an options carrier is optional by construction + # (both aggregate init and a JSON bag let the caller set any subset), so + # it cannot stand in for something the reference demands. A required + # reference param the port lacks is a genuine gap and must keep + # reporting. + # The reference records keyword-ONLY params as ``kind: keyword`` and + # positional-or-keyword ones with no ``kind`` at all; both are settable BY + # NAME, which is the whole contract a carrier carries, so both qualify. + ref_kw = [ + p + for p in ref_params + if p.get("name") + and (p.get("kind") or "positional") + not in ("self", "cls", "var_keyword", "var_positional") + and p.get("required") is False + ] + if not ref_kw: + return + port_names = {p.get("name") for p in port_params} + # Nothing to unfold to if the port already declares them all. + target = [p for p in ref_kw if p["name"] not in port_names] + if not target: + return + # The C++ definition's parameter list has no ``self``, so a port param's + # position in the C++ signature is its index MINUS the leading self/cls. + self_offset = sum( + 1 for p in port_params if (p.get("kind") or "positional") in ("self", "cls") + ) + for idx, p in enumerate(port_params): + if (p.get("kind") or "positional") in ("self", "cls"): + continue + name = p.get("name") + if not name: + continue + # A carrier the REFERENCE also declares by name is the contract itself, + # not a stand-in — leave it typed and whole. + if any(rp.get("name") == name for rp in ref_params): + continue + # A REQUIRED parameter is not an optional-knob carrier. C++ overloads a + # long reference signature as a domain OBJECT the caller must supply + # (``define_tool(const ToolDefinition&)``, + # ``add_language(const LanguageConfig&)``) — the object carries the + # reference's REQUIRED params too (name/description/parameters/handler, + # name/code/voice), which an optional-only unfold would silently drop + # while still consuming the carrier. Measured: without this gate the + # fold turned ``define_tool(tool)`` into ``define_tool(secure)`` and + # ``add_language(lang)`` into five optional knobs, destroying the + # required surface in both. An options carrier the caller may omit + # entirely cannot be standing in for anything mandatory, so requiring + # the carrier itself to be optional is exactly the right discriminator. + if p.get("required") is not False: + continue + ptype = p.get("type", "") + fields = options_by_ref.get(ptype) + if fields is not None: + # TYPED-STRUCT form: unfold only the reference keywords the struct + # genuinely declares a field for. A reference keyword the struct + # LACKS stays missing and keeps reporting as drift — the fold must + # not manufacture a knob the port cannot set. + unfold = [rp for rp in target if rp["name"] in fields] + elif ptype == "any" and _bag_is_spread( + guards, cls, meth, name, idx - self_offset + ): + # UNTYPED-BAG form: a proven wholesale spread reaches every key, so + # every remaining reference knob is settable — but ONLY the ones a + # JSON object can actually hold. A ``const json&`` cannot carry a + # ``std::function``, so unfolding a reference CALLABLE out of it + # would claim a callback the port does not accept. (Measured: the + # fold otherwise invented ``on_completed`` on + # ``detect_answering_machine`` and ``transcribe``.) A callable knob + # the port genuinely lacks stays missing and keeps reporting drift, + # which is the honest result. + unfold = [rp for rp in target if not _is_callable_type(rp.get("type", ""))] + else: + continue + if not unfold: + continue + # Emit each unfolded knob in the reference's OWN kind/type/default. The + # carrier genuinely offers exactly that: settable by name, in any order, + # any subset, defaulting to the reference's default when unset. Copying + # the reference's kind (rather than forcing ``keyword``) keeps the + # keyword-only vs positional-or-keyword distinction the oracle draws. + replacement = [] + for rp in unfold: + np: dict = {"name": rp["name"]} + if rp.get("kind"): + np["kind"] = rp["kind"] + np["type"] = rp.get("type", "any") + np["required"] = False + np["default"] = rp.get("default") + replacement.append(np) + tail = port_params[idx + 1 :] + # A port param the reference declares KEYWORD-ONLY carries the same kind + # as its unfolded siblings. C++ has no keyword arguments at all — every + # parameter is positional — so on a method whose carrier is being + # unfolded, a param that is separately declared purely because its API + # name differs from its wire key (``bind_digit(bind_params)``, + # ``amazon_bedrock(ai_params)``) is reachable by name exactly the way the + # bag's keys are. Recording it ``positional`` next to keyword siblings + # the same fold just emitted would report the carrier's own idiom as + # drift on one param and not the others. + ref_kind = {rp["name"]: rp.get("kind") for rp in ref_params if rp.get("name")} + for i, tp in enumerate(tail): + if ref_kind.get(tp.get("name")) == "keyword" and not tp.get("kind"): + # Rebuild so ``kind`` lands in its usual slot (right after + # ``name``) — the artifact is committed and reviewed, so a + # param record's key order stays uniform across the file. + rebuilt = {"name": tp["name"], "kind": "keyword"} + rebuilt.update({k: v for k, v in tp.items() if k != "name"}) + tail[i] = rebuilt + # ORDER the unfolded knobs (and any optional port params that follow the + # carrier) by the REFERENCE's declaration order. A carrier is unordered + # by nature — a JSON bag and an aggregate initializer both let the caller + # set any subset in any order — but the diff matches params by POSITION, + # so an arbitrary order manufactures param-mismatch findings against the + # neighbouring names. (Measured: ``bind_digit``'s explicit + # ``bind_params``, declared after the bag so the existing call shape + # keeps its meaning, otherwise collided with ``realm``/``max_triggers`` + # and reported three bogus type mismatches.) Port params the reference + # does not name keep their relative order at the end — they are the + # genuine extras and must stay visible as such. + ref_order = {rp["name"]: i for i, rp in enumerate(ref_params) if rp.get("name")} + merged = replacement + [tp for tp in tail if tp.get("required") is False] + rest = [tp for tp in tail if tp.get("required") is not False] + merged.sort(key=lambda x: ref_order.get(x.get("name"), len(ref_order))) + sig["params"] = port_params[:idx] + merged + rest + return + + def _project_callable_shape(out_modules: dict) -> None: """Align C++ ``class:Callable`` with Python's ``callable<...>`` shape. @@ -2128,6 +3690,39 @@ def _load_python_signatures() -> dict: return {} +def _oracle_records_class(ns_str: str, class_name: str, header_path: str) -> bool: + """True when the reference oracle records ``class_name`` in the Python module + this C++ class maps to — the gate that lets a fields-only POD into the + signature inventory. + + Resolves the module the SAME way the emit loop does (CLASS_RENAME_MAP, then + CLASS_MODULE_MAP / module_for_class) so the answer cannot disagree with where + the class would actually land. + + GENERATED DTOs ARE EXCLUDED BY PATH, and that exclusion is load-bearing rather + than cosmetic. ``CLASS_MODULE_MAP`` is keyed by bare class NAME, so it is + namespace-blind: the generated read-side DTO ``signalwire::rest::…::messages:: + Message`` and the generated SWML verb ``…::DataMap`` resolve to the SAME + canonical key as the hand-written ``signalwire.relay.message.Message`` / + ``signalwire.core.data_map.DataMap``. Admitting a DTO therefore does not add a + class — it MERGES its wire fields into the hand-written class's construction + contract (measured: +13 bogus construction params on Message, and DataMap lost + its real ``function_name``). The generated DTOs already reach the audit through + their own generated-payload path; they must not enter here.""" + if "/generated/" in header_path or "_generated/" in header_path: + return False + rename_key = (ns_str, class_name) + if rename_key in CLASS_RENAME_MAP: + mod, cls = CLASS_RENAME_MAP[rename_key] + else: + cls = class_name + mod = CLASS_MODULE_MAP.get(class_name) or module_for_class(class_name, ns_str) + if not mod: + return False + ref = _load_python_signatures() + return cls in ref.get("modules", {}).get(mod, {}).get("classes", {}) + + def _load_python_free_function_targets() -> set[tuple[str, str]]: """Read the Python reference's module-level ``functions`` map so the walker only emits things the Python oracle also exposes at module @@ -2139,15 +3734,14 @@ def _load_python_free_function_targets() -> set[tuple[str, str]]: except FileNotFoundError: return targets for mod, entry in ref.get("modules", {}).items(): - for fn in (entry.get("functions") or {}).keys(): + for fn in entry.get("functions") or {}: targets.add((mod, fn)) return targets -def _translate_with_canonical_fallback(spelling: str, - canonical_spelling: str, - aliases: dict, - ctx: str) -> str: +def _translate_with_canonical_fallback( + spelling: str, canonical_spelling: str, aliases: dict, ctx: str +) -> str: """Translate a C++ type spelling, with awareness of typedef expansion. libclang reports the typedef name in ``arg.type.spelling`` @@ -2180,16 +3774,22 @@ def _translate_with_canonical_fallback(spelling: str, # ``class:.`` invented from the typedef's # bare name, but the typedef actually wraps a stdlib type the # canonical spelling can decompose. - if canonical_spelling and canonical_spelling != spelling and \ - primary.startswith("class:") and "." in primary: + if ( + canonical_spelling + and canonical_spelling != spelling + and primary.startswith("class:") + and "." in primary + ): # Look up the typedef name in CLASS_MODULE_MAP / CLASS_RENAME_MAP # to see if there's an intentional class-rename target. If so, # keep the primary translation; otherwise prefer canonical. tail = primary.split(":", 1)[1] cls_name = tail.rsplit(".", 1)[-1] - if cls_name not in CLASS_MODULE_MAP and \ - not any(v[1] == cls_name for v in CLASS_RENAME_MAP.values()) and \ - cls_name not in CALLBACK_TYPEDEFS_AS_CALLABLE: + if ( + cls_name not in CLASS_MODULE_MAP + and not any(v[1] == cls_name for v in CLASS_RENAME_MAP.values()) + and cls_name not in CALLBACK_TYPEDEFS_AS_CALLABLE + ): try: return translate_cpp_type(canonical_spelling, aliases, ctx) except TypeTranslationError: @@ -2198,16 +3798,27 @@ def _translate_with_canonical_fallback(spelling: str, return primary -def build_signature(method: dict, aliases: dict, context: str) -> dict: +def build_signature( + method: dict, + aliases: dict, + context: str, + *, + guards: GuardIndex | None = None, + cpp_class: str | None = None, + cpp_method: str | None = None, +) -> dict: params_out: list = [] is_static = method.get("is_static", False) is_ctor = method.get("is_constructor", False) if not is_static: params_out.append({"name": "self", "kind": "self"}) - for p in method.get("parameters", []): + for p_index, p in enumerate(method.get("parameters", [])): ctx = f"{context}[{p.get('name')}]" canon_type = _translate_with_canonical_fallback( - p.get("type", ""), p.get("canonical_type", ""), aliases, ctx, + p.get("type", ""), + p.get("canonical_type", ""), + aliases, + ctx, ) param: dict = { "name": p.get("name", "_") or "_", @@ -2215,15 +3826,45 @@ def build_signature(method: dict, aliases: dict, context: str) -> dict: } if p.get("has_default"): param["required"] = False - param["default"] = None + # C++ declares real default arguments, so the VALUE is recoverable + # from the header (unlike languages whose reflection only reports + # that a default exists). A default we could not reduce to a literal + # — an enum value, a constructor call, an arithmetic expression — + # stays ``null``: a documented blind spot, never a guessed value. + dv = p.get("default_value", _NO_DEFAULT) + if dv is _EMPTY_BRACE: + dv = _empty_brace_default(canon_type) + dv = None if dv is _NO_DEFAULT else dv + # THE NULL <-> ZERO-VALUE SENTINEL FOLD (see GuardIndex). A sentinel + # default whose absence the body PROVES with a guard is the C++ + # spelling of the reference's ``= None``; record it as null so the + # two compare equal. An unguarded sentinel is a value the port really + # ships and keeps reporting as drift. + if ( + dv is not None + and guards is not None + and cpp_class + and cpp_method + and _is_foldable_sentinel(canon_type, dv) + ): + kind = _sentinel_kind(canon_type) + if kind and guards.guards( + cpp_class, cpp_method, p.get("name") or "", p_index, kind + ): + dv = None + param["default"] = dv else: param["required"] = True params_out.append(param) - return_canon = "void" if is_ctor else _translate_with_canonical_fallback( - method.get("return_type", "void"), - method.get("canonical_return_type", ""), - aliases, - context + "[->]", + return_canon = ( + "void" + if is_ctor + else _translate_with_canonical_fallback( + method.get("return_type", "void"), + method.get("canonical_return_type", ""), + aliases, + context + "[->]", + ) ) return {"params": params_out, "returns": return_canon} @@ -2232,7 +3873,27 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--include", type=Path, default=PORT_ROOT / "include") parser.add_argument("--out", type=Path, default=PORT_ROOT / "port_signatures.json") - parser.add_argument("--strict", action="store_true") + # FAIL-LOUD BY DEFAULT (2026-07-30). This used to be an opt-in + # `--strict` that NOT ONE of the six gate invocations passed — run-ci.sh, + # porting-sdk/scripts/suites/_signatures_fresh.py:156/163 and + # _surface_commands.py:448/481/535/562/589/635 all call this script bare. So + # the fail-loud path was dead code, and a translation failure printed a + # warning and STILL EXITED 0 while the affected symbol vanished from + # port_signatures.json entirely. Measured on this repo before the fix: 5 + # failures at rc=0, with ContextBuilder.attach_tool_name_supplier and + # SWMLService.generate_random_hex silently absent despite being declared. + # + # BooleanOptionalAction with default=True means the six existing callers need + # no change — they simply start failing loud. `--no-strict` is the local-only + # escape hatch for deliberately inspecting a partial artifact. `--strict` + # still parses, so the documented invocation keeps working. + parser.add_argument( + "--strict", + action=argparse.BooleanOptionalAction, + default=True, + help="fail (exit 1) on any type-translation failure instead of silently " + "emitting an artifact that omits the affected symbols (default: enabled)", + ) args = parser.parse_args() aliases = load_aliases() @@ -2310,8 +3971,11 @@ def main() -> int: raw_options_structs.update(opt_structs) single_tu_ok = True except Exception as e: - print(f"enumerate_signatures: single-TU parse failed ({e}); " - f"falling back to per-header", file=sys.stderr) + print( + f"enumerate_signatures: single-TU parse failed ({e}); " + f"falling back to per-header", + file=sys.stderr, + ) if not single_tu_ok: raw_entries.clear() @@ -2323,26 +3987,54 @@ def main() -> int: except Exception as e: print(f"skip {header}: {e}", file=sys.stderr) continue - cls_entries, fn_entries, opt_structs = walk_translation_unit(tu, args.include) + cls_entries, fn_entries, opt_structs = walk_translation_unit( + tu, args.include + ) raw_entries.extend(cls_entries) raw_free_functions.extend(fn_entries) raw_options_structs.update(opt_structs) - canonical, failures = collect(raw_entries, aliases, raw_free_functions, - raw_options_structs) + # Body index for the null <-> zero-value sentinel fold. Built from the + # implementation tree + the headers (inline bodies); see GuardIndex. + guards = GuardIndex([PORT_ROOT / "src", args.include]) + + canonical, failures = collect( + raw_entries, aliases, raw_free_functions, raw_options_structs, guards=guards + ) if failures: - print(f"enumerate_signatures: {len(failures)} translation failure(s)", file=sys.stderr) + print( + f"enumerate_signatures: {len(failures)} translation failure(s)", + file=sys.stderr, + ) for f in failures[:30]: print(f" - {f}", file=sys.stderr) if len(failures) > 30: print(f" ... ({len(failures) - 30} more)", file=sys.stderr) if args.strict: + print( + "enumerate_signatures: REFUSING to write a signature artifact that " + "silently OMITS the symbols above. A failed translation drops that " + "method from port_signatures.json entirely, so the SIGNATURES / DRIFT " + "gates would compare against a surface this port does not actually " + "have. Add the type to porting-sdk/type_aliases.yaml under aliases.cpp " + "if it is real vocabulary, or make the member non-public if it is an " + "internal seam. --no-strict emits the partial artifact anyway (local " + "inspection only; no gate should ever pass it).", + file=sys.stderr, + ) return 1 - args.out.write_text(json.dumps(canonical, indent=2, sort_keys=False) + "\n", encoding="utf-8") + args.out.write_text( + json.dumps(canonical, indent=2, sort_keys=False) + "\n", encoding="utf-8" + ) n_mods = len(canonical["modules"]) - n_methods = sum(sum(len(c["methods"]) for c in m.get("classes", {}).values()) for m in canonical["modules"].values()) - print(f"enumerate_signatures: wrote {args.out} ({n_mods} modules, {n_methods} methods)") + n_methods = sum( + sum(len(c["methods"]) for c in m.get("classes", {}).values()) + for m in canonical["modules"].values() + ) + print( + f"enumerate_signatures: wrote {args.out} ({n_mods} modules, {n_methods} methods)" + ) return 0 diff --git a/scripts/enumerate_surface.py b/scripts/enumerate_surface.py index 091247e..018697c 100644 --- a/scripts/enumerate_surface.py +++ b/scripts/enumerate_surface.py @@ -73,6 +73,7 @@ def _resolve_psdk() -> Path: return Path(val).resolve() return (Path(__file__).resolve().parent.parent.parent / "porting-sdk").resolve() + # --------------------------------------------------------------------------- # Class -> Python module mapping # --------------------------------------------------------------------------- @@ -86,28 +87,23 @@ def _resolve_psdk() -> Path: CLASS_MODULE_MAP: dict[str, str] = { # -- agent ------------------------------------------------------------ "AgentBase": "signalwire.core.agent_base", - # -- pom -------------------------------------------------------------- # PromptObjectModel has no name conflict; Section does (swml::Section). # PromptObjectModel can use the simple class-name map; Section is # disambiguated via CLASS_RENAME_MAP keyed on (signalwire::pom, Section). "PromptObjectModel": "signalwire.pom.pom", - # -- contexts --------------------------------------------------------- "Context": "signalwire.core.contexts", "ContextBuilder": "signalwire.core.contexts", "GatherInfo": "signalwire.core.contexts", "GatherQuestion": "signalwire.core.contexts", "Step": "signalwire.core.contexts", - # -- datamap ---------------------------------------------------------- "DataMap": "signalwire.core.data_map", - # -- swaig ------------------------------------------------------------ "FunctionResult": "signalwire.core.function_result", "ToolDefinition": "signalwire.core.swaig_function", "SWAIGFunction": "signalwire.core.swaig_function", - # -- swml verb-handler registry (core/swml_handler.hpp) --------------- "SWMLVerbHandler": "signalwire.core.swml_handler", "AIVerbHandler": "signalwire.core.swml_handler", @@ -115,39 +111,39 @@ def _resolve_psdk() -> Path: # -- swml builder / renderer ----------------------------------------- "SWMLBuilder": "signalwire.core.swml_builder", "SwmlRenderer": "signalwire.core.swml_renderer", - # -- core infra classes (auth/config/security/pom) -------------------- "AuthHandler": "signalwire.core.auth_handler", + # The credential carriers live BESIDE AuthHandler in the reference module + # (the oracle records signalwire.core.auth_handler.BasicCredentials / + # .BearerCredentials since porting-sdk dcff742 resolved the FastAPI names). + # Without this the port-only fallback would snake_case the class name into + # its own module leaf (signalwire.core.basic_credentials) and the carriers + # would never meet their reference counterparts. + "BasicCredentials": "signalwire.core.auth_handler", + "BearerCredentials": "signalwire.core.auth_handler", "ConfigLoader": "signalwire.core.config_loader", "SecurityConfig": "signalwire.core.security_config", "PomBuilder": "signalwire.core.pom_builder", - # -- skills ----------------------------------------------------------- "SkillBase": "signalwire.core.skill_base", "SkillManager": "signalwire.core.skill_manager", "SkillRegistry": "signalwire.skills.registry", - # -- prefab agents ---------------------------------------------------- "BedrockAgent": "signalwire.agents.bedrock", - # -- server ----------------------------------------------------------- "AgentServer": "signalwire.agent_server", - # -- security --------------------------------------------------------- "SessionManager": "signalwire.core.security.session_manager", - # -- swml ------------------------------------------------------------- # Document/Schema have no exact Python analog, so treat as port-only # via the native translation. # ``Service`` in C++ == Python's ``SWMLService``; rename at emit time. # Handled via CLASS_RENAME_MAP below, not via module mapping. - # -- utils ------------------------------------------------------------ # SchemaUtils + SchemaValidationError both live under # signalwire.utils.schema_utils per the canonical Python module layout. "SchemaUtils": "signalwire.utils.schema_utils", "SchemaValidationError": "signalwire.utils.schema_utils", - # -- rest ------------------------------------------------------------- "HttpClient": "signalwire.rest._base", "CrudResource": "signalwire.rest._base", @@ -176,7 +172,6 @@ def _resolve_psdk() -> Path: "SipProfileNamespace": "signalwire.rest.namespaces.sip_profile", "VerifiedCallersNamespace": "signalwire.rest.namespaces.verified_callers", "VideoNamespace": "signalwire.rest.namespaces.video", - # -- rest sub-resources (Python parity) ------------------------------- # Fabric sub-resources. "FabricAddresses": "signalwire.rest.namespaces.fabric", @@ -188,19 +183,16 @@ def _resolve_psdk() -> Path: "FabricResourcePUT": "signalwire.rest.namespaces.fabric", "FabricSubscribers": "signalwire.rest.namespaces.fabric", "FabricTokens": "signalwire.rest.namespaces.fabric", - # Logs sub-resources. "LogsConferences": "signalwire.rest.namespaces.logs", "LogsFax": "signalwire.rest.namespaces.logs", "LogsMessages": "signalwire.rest.namespaces.logs", "LogsVoice": "signalwire.rest.namespaces.logs", - # Registry sub-resources. "RegistryBrands": "signalwire.rest.namespaces.registry", "RegistryCampaigns": "signalwire.rest.namespaces.registry", "RegistryNumbers": "signalwire.rest.namespaces.registry", "RegistryOrders": "signalwire.rest.namespaces.registry", - # Video sub-resources. "VideoConferences": "signalwire.rest.namespaces.video", "VideoConferenceTokens": "signalwire.rest.namespaces.video", @@ -209,10 +201,8 @@ def _resolve_psdk() -> Path: "VideoRoomTokens": "signalwire.rest.namespaces.video", "VideoRooms": "signalwire.rest.namespaces.video", "VideoStreams": "signalwire.rest.namespaces.video", - # Pagination helper -- Python: signalwire.rest._pagination.PaginatedIterator. "PaginatedIterator": "signalwire.rest._pagination", - # -- relay ------------------------------------------------------------ "RelayClient": "signalwire.relay.client", "RelayError": "signalwire.relay.client", @@ -221,14 +211,12 @@ def _resolve_psdk() -> Path: # Action / RelayEvent / CallEvent / MessageEvent / DialEvent / # ComponentEvent have no 1:1 Python analog -- port-only, use native # translation. - # -- prefabs ---------------------------------------------------------- "ConciergeAgent": "signalwire.prefabs.concierge", "FAQBotAgent": "signalwire.prefabs.faq_bot", "InfoGathererAgent": "signalwire.prefabs.info_gatherer", "ReceptionistAgent": "signalwire.prefabs.receptionist", "SurveyAgent": "signalwire.prefabs.survey", - # -- logging ---------------------------------------------------------- # ``Logger`` in signalwire::logging -> Python core.logging_config # has no matching class (Python uses module-level functions), @@ -243,13 +231,15 @@ def _resolve_psdk() -> Path: CLASS_RENAME_MAP: dict[tuple[str, str], tuple[str, str]] = { # (source_ns, source_class) -> (target_module, target_class) ("signalwire::swml", "Service"): ( - "signalwire.core.swml_service", "SWMLService", + "signalwire.core.swml_service", + "SWMLService", ), # ``signalwire::pom::Section`` projects to ``signalwire.pom.pom.Section``; # disambiguates from ``signalwire::swml::Section`` (which falls through # to the native namespace translation as ``signalwire.swml.section``). ("signalwire::pom", "Section"): ( - "signalwire.pom.pom", "Section", + "signalwire.pom.pom", + "Section", ), # C++ uses ``XxxNamespace`` for all REST namespaces; Python uses # ``XxxResource`` for single-resource namespaces and ``XxxNamespace`` @@ -260,90 +250,114 @@ def _resolve_psdk() -> Path: # method — the value struct's data fields (timeout/retries/…) are not surface # symbols, exactly as the Python dataclass fields aren't (go/ts/ruby/java match). ("signalwire::rest", "RequestOptions"): ( - "signalwire.rest._request_options", "RequestOptions", + "signalwire.rest._request_options", + "RequestOptions", ), ("signalwire::rest", "AddressesNamespace"): ( - "signalwire.rest.namespaces.addresses", "AddressesResource", + "signalwire.rest.namespaces.addresses", + "AddressesResource", ), ("signalwire::rest", "ChatNamespace"): ( - "signalwire.rest.namespaces.chat", "ChatResource", + "signalwire.rest.namespaces.chat", + "ChatResource", ), ("signalwire::rest", "ImportedNumbersNamespace"): ( - "signalwire.rest.namespaces.imported_numbers", "ImportedNumbersResource", + "signalwire.rest.namespaces.imported_numbers", + "ImportedNumbersResource", ), ("signalwire::rest", "LookupNamespace"): ( - "signalwire.rest.namespaces.lookup", "LookupResource", + "signalwire.rest.namespaces.lookup", + "LookupResource", ), ("signalwire::rest", "MFANamespace"): ( - "signalwire.rest.namespaces.mfa", "MfaResource", + "signalwire.rest.namespaces.mfa", + "MfaResource", ), ("signalwire::rest", "NumberGroupsNamespace"): ( - "signalwire.rest.namespaces.number_groups", "NumberGroupsResource", + "signalwire.rest.namespaces.number_groups", + "NumberGroupsResource", ), ("signalwire::rest", "PhoneNumbersNamespace"): ( - "signalwire.rest.namespaces.phone_numbers", "PhoneNumbersResource", + "signalwire.rest.namespaces.phone_numbers", + "PhoneNumbersResource", ), ("signalwire::rest", "PubSubNamespace"): ( - "signalwire.rest.namespaces.pubsub", "PubSubResource", + "signalwire.rest.namespaces.pubsub", + "PubSubResource", ), ("signalwire::rest", "QueuesNamespace"): ( - "signalwire.rest.namespaces.queues", "QueuesResource", + "signalwire.rest.namespaces.queues", + "QueuesResource", ), ("signalwire::rest", "RecordingsNamespace"): ( - "signalwire.rest.namespaces.recordings", "RecordingsResource", + "signalwire.rest.namespaces.recordings", + "RecordingsResource", ), ("signalwire::rest", "ShortCodesNamespace"): ( - "signalwire.rest.namespaces.short_codes", "ShortCodesResource", + "signalwire.rest.namespaces.short_codes", + "ShortCodesResource", ), ("signalwire::rest", "SipProfileNamespace"): ( - "signalwire.rest.namespaces.sip_profile", "SipProfileResource", + "signalwire.rest.namespaces.sip_profile", + "SipProfileResource", ), ("signalwire::rest", "VerifiedCallersNamespace"): ( - "signalwire.rest.namespaces.verified_callers", "VerifiedCallersResource", + "signalwire.rest.namespaces.verified_callers", + "VerifiedCallersResource", ), # ProjectTokens is exposed as a nested class on the project namespace. ("signalwire::rest", "ProjectTokens"): ( - "signalwire.rest.namespaces.project", "ProjectTokens", + "signalwire.rest.namespaces.project", + "ProjectTokens", ), # DatasphereDocuments is the typed wrapper around the documents # CrudResource; Python exposes it as DatasphereDocuments inside # namespaces/datasphere.py. ("signalwire::rest", "DatasphereDocuments"): ( - "signalwire.rest.namespaces.datasphere", "DatasphereDocuments", + "signalwire.rest.namespaces.datasphere", + "DatasphereDocuments", ), # Fabric: C++ uses ``FabricXxx`` names for sub-resources; Python uses # ``XxxResource`` (or shorter names). Map at emit time so the audit # treats them as the same class. ("signalwire::rest", "FabricCallFlows"): ( - "signalwire.rest.namespaces.fabric", "CallFlowsResource", + "signalwire.rest.namespaces.fabric", + "CallFlowsResource", ), ("signalwire::rest", "FabricConferenceRooms"): ( - "signalwire.rest.namespaces.fabric", "ConferenceRoomsResource", + "signalwire.rest.namespaces.fabric", + "ConferenceRoomsResource", ), ("signalwire::rest", "FabricCxmlApplications"): ( - "signalwire.rest.namespaces.fabric", "CxmlApplicationsResource", + "signalwire.rest.namespaces.fabric", + "CxmlApplicationsResource", ), ("signalwire::rest", "FabricGenericResources"): ( - "signalwire.rest.namespaces.fabric", "GenericResources", + "signalwire.rest.namespaces.fabric", + "GenericResources", ), ("signalwire::rest", "FabricSubscribers"): ( - "signalwire.rest.namespaces.fabric", "SubscribersResource", + "signalwire.rest.namespaces.fabric", + "SubscribersResource", ), # Logs: Python names are ``MessageLogs`` / ``VoiceLogs`` etc; C++ uses # ``LogsMessages`` / ``LogsVoice`` for namespace-prefix consistency. ("signalwire::rest", "LogsMessages"): ( - "signalwire.rest.namespaces.logs", "MessageLogs", + "signalwire.rest.namespaces.logs", + "MessageLogs", ), ("signalwire::rest", "LogsVoice"): ( - "signalwire.rest.namespaces.logs", "VoiceLogs", + "signalwire.rest.namespaces.logs", + "VoiceLogs", ), ("signalwire::rest", "LogsFax"): ( - "signalwire.rest.namespaces.logs", "FaxLogs", + "signalwire.rest.namespaces.logs", + "FaxLogs", ), ("signalwire::rest", "LogsConferences"): ( - "signalwire.rest.namespaces.logs", "ConferenceLogs", + "signalwire.rest.namespaces.logs", + "ConferenceLogs", ), - # -- Callback typedef projection ------------------------------------- # C++ uses ``using XxxHandler = std::function<...>`` aliases for # callbacks. libclang emits the typedef name (``InboundCallHandler`` @@ -353,10 +367,12 @@ def _resolve_psdk() -> Path: # Map at emit time so the diff treats handler signatures as the # same callable contract regardless of the C++ typedef name. ("signalwire::relay", "InboundCallHandler"): ( - "signalwire.relay.client", "CallHandler", + "signalwire.relay.client", + "CallHandler", ), ("signalwire::relay", "InboundMessageHandler"): ( - "signalwire.relay.client", "MessageHandler", + "signalwire.relay.client", + "MessageHandler", ), } @@ -378,11 +394,19 @@ def _resolve_psdk() -> Path: # native-namespace translation (signalwire.rest.generated.) and drift. def _load_generated_surface_map() -> dict[str, str]: here = Path(__file__).resolve().parent - smap = (here.parent / "include" / "signalwire" / "rest" / "namespaces" - / "generated" / "generated_surface_map.json") + smap = ( + here.parent + / "include" + / "signalwire" + / "rest" + / "namespaces" + / "generated" + / "generated_surface_map.json" + ) if not smap.is_file(): return {} import json as _json + return _json.loads(smap.read_text()) @@ -426,10 +450,20 @@ def _load_generated_surface_map() -> dict[str, str]: # groups map their whole namespace prefix to the flat reference module. _TYPES_NS_PREFIX = "signalwire::rest::generated::types::" _TYPES_NS_KEY = { - "RelayRest": "relay_rest", "Fabric": "fabric", "Calling": "calling", - "Video": "video", "Datasphere": "datasphere", "Logs": "logs", - "Message": "message", "Messages": "messages", "Voice": "voice", "Fax": "fax", "Project": "project", - "Projects": "projects", "Chat": "chat", "PubSub": "pubsub", + "RelayRest": "relay_rest", + "Fabric": "fabric", + "Calling": "calling", + "Video": "video", + "Datasphere": "datasphere", + "Logs": "logs", + "Message": "message", + "Messages": "messages", + "Voice": "voice", + "Fax": "fax", + "Project": "project", + "Projects": "projects", + "Chat": "chat", + "PubSub": "pubsub", "SwmlWebhooks": "swml_webhooks", } GENERATED_PAYLOAD_NS = { @@ -441,11 +475,14 @@ def _load_generated_surface_map() -> dict[str, str]: } # Namespace-path prefixes whose classes are generated method-less types (used by # parse_header to force-register a zero-method struct so it surfaces). -GENERATED_TYPE_NS_PREFIXES = (_TYPES_NS_PREFIX.rstrip(":"),) + tuple(GENERATED_PAYLOAD_NS) +GENERATED_TYPE_NS_PREFIXES = ( + _TYPES_NS_PREFIX.rstrip(":"), + *tuple(GENERATED_PAYLOAD_NS), +) # Set at build_snapshot entry: the ``…/include`` root under which the generated # payload header namespaces resolve (``signalwire::core::foo`` -> /signalwire/core/foo). -_INCLUDE_ROOT: Path = Path(".") +_INCLUDE_ROOT: Path = Path() def generated_type_module(ns_path: str) -> str | None: @@ -454,12 +491,13 @@ def generated_type_module(ns_path: str) -> str | None: if ns_path in GENERATED_PAYLOAD_NS: return GENERATED_PAYLOAD_NS[ns_path] if ns_path.startswith(_TYPES_NS_PREFIX): - seg = ns_path[len(_TYPES_NS_PREFIX):].split("::", 1)[0] + seg = ns_path[len(_TYPES_NS_PREFIX) :].split("::", 1)[0] key = _TYPES_NS_KEY.get(seg) if key is None: raise SystemExit( f"enumerate_surface.py: generated types namespace {ns_path!r} has " - f"unknown segment {seg!r} (add to _TYPES_NS_KEY)") + f"unknown segment {seg!r} (add to _TYPES_NS_KEY)" + ) return f"signalwire.rest.namespaces.{key}_types_generated" return None @@ -527,10 +565,12 @@ def _is_generated_type_ns(ns_path: str) -> bool: # Webhook signature validation (porting-sdk/webhooks.md). C++ uses # PascalCase per its naming convention; Python uses snake_case. ("signalwire::security", "ValidateWebhookSignature"): ( - "signalwire.core.security.webhook_validator", "validate_webhook_signature", + "signalwire.core.security.webhook_validator", + "validate_webhook_signature", ), ("signalwire::security", "ValidateRequest"): ( - "signalwire.core.security.webhook_validator", "validate_request", + "signalwire.core.security.webhook_validator", + "validate_request", ), # The framework-free webhook-validation decision core (porting-sdk # webhooks.md + HIDDEN_SURFACE_AUDIT Pass 1). Python exposes it as a @@ -540,30 +580,36 @@ def _is_generated_type_ns(ns_path: str) -> bool: # cpp-httplib ``WrapWithSignatureValidation`` wrapper stays a # PORT_ADDITION idiom on top of this. ("signalwire::security", "Validate"): ( - "signalwire.core.security.webhook_middleware", "validate", + "signalwire.core.security.webhook_middleware", + "validate", ), # Standalone security-hygiene utils (security_utils.py). C++ groups them in # a nested ``signalwire::security::security_utils`` namespace with PascalCase # names; Python keeps them as module-level snake_case functions under # ``signalwire.core.security.security_utils``. ("signalwire::security::security_utils", "FilterSensitiveHeaders"): ( - "signalwire.core.security.security_utils", "filter_sensitive_headers", + "signalwire.core.security.security_utils", + "filter_sensitive_headers", ), ("signalwire::security::security_utils", "RedactUrl"): ( - "signalwire.core.security.security_utils", "redact_url", + "signalwire.core.security.security_utils", + "redact_url", ), ("signalwire::security::security_utils", "IsValidHostname"): ( - "signalwire.core.security.security_utils", "is_valid_hostname", + "signalwire.core.security.security_utils", + "is_valid_hostname", ), # SWAIG schema inference (type_inference.py). C++ groups these in a nested # ``signalwire::swaig::type_inference`` namespace (snake_case names); # Python keeps them module-level under # ``signalwire.core.agent.tools.type_inference``. ("signalwire::swaig::type_inference", "infer_schema"): ( - "signalwire.core.agent.tools.type_inference", "infer_schema", + "signalwire.core.agent.tools.type_inference", + "infer_schema", ), ("signalwire::swaig::type_inference", "create_typed_handler_wrapper"): ( - "signalwire.core.agent.tools.type_inference", "create_typed_handler_wrapper", + "signalwire.core.agent.tools.type_inference", + "create_typed_handler_wrapper", ), } @@ -577,7 +623,13 @@ def _is_generated_type_ns(ns_path: str) -> bool: "friend", "template", "return", - "if", "else", "for", "while", "do", "switch", "case", + "if", + "else", + "for", + "while", + "do", + "switch", + "case", } @@ -605,29 +657,47 @@ def _is_generated_type_ns(ns_path: str) -> bool: # (module_path, class_name) -> list of method names to copy from # the C++ AgentBase class if present there. ("signalwire.core.mixins.ai_config_mixin", "AIConfigMixin"): [ - "add_function_include", "add_hint", "add_hints", "add_internal_filler", - "add_language", "add_mcp_server", "add_pattern_hint", "add_pronunciation", - "enable_debug_events", "enable_mcp_server", + "add_function_include", + "add_hint", + "add_hints", + "add_internal_filler", + "add_language", + "add_mcp_server", + "add_pattern_hint", + "add_pronunciation", + "enable_debug_events", + "enable_mcp_server", "get_language_params", - "set_function_includes", "set_global_data", "set_internal_fillers", + "set_function_includes", + "set_global_data", + "set_internal_fillers", "set_language_params", - "set_languages", "set_multilingual", "set_native_functions", "set_param", "set_params", - "set_post_prompt_llm_params", "set_prompt_llm_params", - "set_pronunciations", "update_global_data", - ], - ("signalwire.core.mixins.auth_mixin", "AuthMixin"): [ - # These two AuthMixin methods are implementation-detail protected - # helpers in C++ (validate_auth) that aren't part of the public C++ - # surface. Tracked as a PORT_OMISSIONS exemption, not a projection. + "set_languages", + "set_multilingual", + "set_native_functions", + "set_param", + "set_params", + "set_post_prompt_llm_params", + "set_prompt_llm_params", + "set_pronunciations", + "update_global_data", ], ("signalwire.core.mixins.mcp_server_mixin", "MCPServerMixin"): [ # Empty in Python -- class exists as a marker only. ], ("signalwire.core.mixins.prompt_mixin", "PromptMixin"): [ - "contexts", "define_contexts", "get_post_prompt", "get_prompt", - "prompt_add_section", "prompt_add_subsection", "prompt_add_to_section", - "prompt_has_section", "reset_contexts", "set_post_prompt", - "set_prompt_pom", "set_prompt_text", + "contexts", + "define_contexts", + "get_post_prompt", + "get_prompt", + "prompt_add_section", + "prompt_add_subsection", + "prompt_add_to_section", + "prompt_has_section", + "reset_contexts", + "set_post_prompt", + "set_prompt_pom", + "set_prompt_text", ], # Python additionally extracted a ``PromptManager`` class that # PromptMixin delegates to. The user-facing surface is identical @@ -643,10 +713,18 @@ def _is_generated_type_ns(ns_path: str) -> bool: # the fold of that merge: reaching the agent from the manager is exactly as # available in C++ as in Python, it is simply already in hand. ("signalwire.core.agent.prompt.manager", "PromptManager"): [ - "__init__", "define_contexts", "get_contexts", "get_post_prompt", "get_prompt", + "__init__", + "define_contexts", + "get_contexts", + "get_post_prompt", + "get_prompt", "get_raw_prompt", - "prompt_add_section", "prompt_add_subsection", "prompt_add_to_section", - "prompt_has_section", "set_post_prompt", "set_prompt_pom", + "prompt_add_section", + "prompt_add_subsection", + "prompt_add_to_section", + "prompt_has_section", + "set_post_prompt", + "set_prompt_pom", "set_prompt_text", ], ("signalwire.core.mixins.serverless_mixin", "ServerlessMixin"): [ @@ -656,26 +734,55 @@ def _is_generated_type_ns(ns_path: str) -> bool: "handle_serverless_request", ], ("signalwire.core.mixins.skill_mixin", "SkillMixin"): [ - "add_skill", "has_skill", "list_skills", "remove_skill", + "add_skill", + "has_skill", + "list_skills", + "remove_skill", ], ("signalwire.core.mixins.state_mixin", "StateMixin"): [ "validate_tool_token", ], ("signalwire.core.mixins.tool_mixin", "ToolMixin"): [ - "define_tool", "define_tools", "on_function_call", "register_swaig_function", + "define_tool", + "define_tools", + "on_function_call", + "register_swaig_function", ], ("signalwire.core.agent.tools.registry", "ToolRegistry"): [ - "__init__", "define_tool", "register_swaig_function", - "has_function", "get_function", "get_all_functions", + "__init__", + "define_tool", + "register_swaig_function", + "has_function", + "get_function", + "get_all_functions", "remove_function", ], + # Both methods ARE public C++ surface — swml::Service declares them at + # include/signalwire/swml/service.hpp:96 and :100 — so they project. + # + # This key was previously declared TWICE in this dict: an earlier stanza bound + # it to an empty list with a comment asserting the two methods were + # "implementation-detail protected helpers ... not part of the public C++ + # surface. Tracked as a PORT_OMISSIONS exemption, not a projection." That + # comment was false on both counts (the methods are public, and PORT_OMISSIONS + # has no such entry), and because a later duplicate key silently wins in a + # Python dict literal, the empty stanza had never had any effect. The dead + # stanza is deleted; this one is and always was the live binding. ("signalwire.core.mixins.auth_mixin", "AuthMixin"): [ - "validate_basic_auth", "get_basic_auth_credentials", + "validate_basic_auth", + "get_basic_auth_credentials", ], ("signalwire.core.mixins.web_mixin", "WebMixin"): [ - "as_router", "enable_debug_routes", "manual_set_proxy_url", "run", - "serve", "set_dynamic_config_callback", "on_request", "on_swml_request", - "register_routing_callback", "setup_graceful_shutdown", + "as_router", + "enable_debug_routes", + "manual_set_proxy_url", + "run", + "serve", + "set_dynamic_config_callback", + "on_request", + "on_swml_request", + "register_routing_callback", + "setup_graceful_shutdown", ], } @@ -730,48 +837,208 @@ def _is_generated_type_ns(ns_path: str) -> bool: # Methods that live on the shared C++ ``SkillBase`` (so every concrete skill # inherits them and they are legitimately part of that skill's callable surface). _SKILL_BASE_METHODS = { - "setup", "register_tools", "get_hints", "get_global_data", - "get_prompt_sections", "get_parameter_schema", "get_instance_key", - "cleanup", "get_datamap_functions", "skill_name", "skill_description", + "setup", + "register_tools", + "get_hints", + "get_global_data", + "get_prompt_sections", + "get_parameter_schema", + "get_instance_key", + "cleanup", + "get_datamap_functions", + "skill_name", + "skill_description", } SKILL_PROJECTIONS: dict[str, tuple[str, str, list[str]]] = { # cpp_class: (python_module, python_class, python_recorded_methods) - "ApiNinjasTriviaSkill": ("signalwire.skills.api_ninjas_trivia.skill", "ApiNinjasTriviaSkill", - ["__init__", "get_instance_key", "get_parameter_schema", "get_tools", "register_tools", "setup"]), - "ClaudeSkillsSkill": ("signalwire.skills.claude_skills.skill", "ClaudeSkillsSkill", - ["get_hints", "get_instance_key", "get_parameter_schema", "register_tools", "setup"]), - "DatasphereSkill": ("signalwire.skills.datasphere.skill", "DataSphereSkill", - ["cleanup", "get_global_data", "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup"]), - "DatasphereServerlessSkill": ("signalwire.skills.datasphere_serverless.skill", "DataSphereServerlessSkill", - ["get_global_data", "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup"]), - "DateTimeSkill": ("signalwire.skills.datetime.skill", "DateTimeSkill", - ["get_hints", "get_parameter_schema", "get_prompt_sections", "register_tools", "setup"]), - "GoogleMapsSkill": ("signalwire.skills.google_maps.skill", "GoogleMapsSkill", - ["get_hints", "get_parameter_schema", "get_prompt_sections", "register_tools", "setup"]), - "InfoGathererSkill": ("signalwire.skills.info_gatherer.skill", "InfoGathererSkill", - ["get_global_data", "get_instance_key", "get_parameter_schema", "register_tools", "setup"]), - "JokeSkill": ("signalwire.skills.joke.skill", "JokeSkill", - ["get_global_data", "get_hints", "get_parameter_schema", "get_prompt_sections", "register_tools", "setup"]), - "MathSkill": ("signalwire.skills.math.skill", "MathSkill", - ["get_hints", "get_parameter_schema", "get_prompt_sections", "register_tools", "setup"]), - "NativeVectorSearchSkill": ("signalwire.skills.native_vector_search.skill", "NativeVectorSearchSkill", - ["cleanup", "get_global_data", "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup"]), - "PlayBackgroundFileSkill": ("signalwire.skills.play_background_file.skill", "PlayBackgroundFileSkill", - ["__init__", "get_instance_key", "get_parameter_schema", "get_tools", "register_tools", "setup"]), - "SpiderSkill": ("signalwire.skills.spider.skill", "SpiderSkill", - ["__init__", "cleanup", "get_hints", "get_instance_key", "get_parameter_schema", "register_tools", "setup"]), - "SwmlTransferSkill": ("signalwire.skills.swml_transfer.skill", "SWMLTransferSkill", - ["get_hints", "get_instance_key", "get_parameter_schema", "get_prompt_sections", "register_tools", "setup"]), - "WeatherApiSkill": ("signalwire.skills.weather_api.skill", "WeatherApiSkill", - ["__init__", "get_parameter_schema", "get_tools", "register_tools", "setup"]), - "WebSearchSkill": ("signalwire.skills.web_search.skill", "WebSearchSkill", - ["get_global_data", "get_hints", "get_instance_key", "get_parameter_schema", - "get_prompt_sections", "register_tools", "setup"]), - "WikipediaSearchSkill": ("signalwire.skills.wikipedia_search.skill", "WikipediaSearchSkill", - ["get_hints", "get_parameter_schema", "get_prompt_sections", "register_tools", "search_wiki", "setup"]), + "ApiNinjasTriviaSkill": ( + "signalwire.skills.api_ninjas_trivia.skill", + "ApiNinjasTriviaSkill", + [ + "__init__", + "get_instance_key", + "get_parameter_schema", + "get_tools", + "register_tools", + "setup", + ], + ), + "ClaudeSkillsSkill": ( + "signalwire.skills.claude_skills.skill", + "ClaudeSkillsSkill", + [ + "get_hints", + "get_instance_key", + "get_parameter_schema", + "register_tools", + "setup", + ], + ), + "DatasphereSkill": ( + "signalwire.skills.datasphere.skill", + "DataSphereSkill", + [ + "cleanup", + "get_global_data", + "get_hints", + "get_instance_key", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "DatasphereServerlessSkill": ( + "signalwire.skills.datasphere_serverless.skill", + "DataSphereServerlessSkill", + [ + "get_global_data", + "get_hints", + "get_instance_key", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "DateTimeSkill": ( + "signalwire.skills.datetime.skill", + "DateTimeSkill", + [ + "get_hints", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "GoogleMapsSkill": ( + "signalwire.skills.google_maps.skill", + "GoogleMapsSkill", + [ + "get_hints", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "InfoGathererSkill": ( + "signalwire.skills.info_gatherer.skill", + "InfoGathererSkill", + [ + "get_global_data", + "get_instance_key", + "get_parameter_schema", + "register_tools", + "setup", + ], + ), + "JokeSkill": ( + "signalwire.skills.joke.skill", + "JokeSkill", + [ + "get_global_data", + "get_hints", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "MathSkill": ( + "signalwire.skills.math.skill", + "MathSkill", + [ + "get_hints", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "NativeVectorSearchSkill": ( + "signalwire.skills.native_vector_search.skill", + "NativeVectorSearchSkill", + [ + "cleanup", + "get_global_data", + "get_hints", + "get_instance_key", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "PlayBackgroundFileSkill": ( + "signalwire.skills.play_background_file.skill", + "PlayBackgroundFileSkill", + [ + "__init__", + "get_instance_key", + "get_parameter_schema", + "get_tools", + "register_tools", + "setup", + ], + ), + "SpiderSkill": ( + "signalwire.skills.spider.skill", + "SpiderSkill", + [ + "__init__", + "cleanup", + "get_hints", + "get_instance_key", + "get_parameter_schema", + "register_tools", + "remove_xpaths", + "setup", + ], + ), + "SwmlTransferSkill": ( + "signalwire.skills.swml_transfer.skill", + "SWMLTransferSkill", + [ + "get_hints", + "get_instance_key", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "WeatherApiSkill": ( + "signalwire.skills.weather_api.skill", + "WeatherApiSkill", + ["__init__", "get_parameter_schema", "get_tools", "register_tools", "setup"], + ), + "WebSearchSkill": ( + "signalwire.skills.web_search.skill", + "WebSearchSkill", + [ + "get_global_data", + "get_hints", + "get_instance_key", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "setup", + ], + ), + "WikipediaSearchSkill": ( + "signalwire.skills.wikipedia_search.skill", + "WikipediaSearchSkill", + [ + "get_hints", + "get_parameter_schema", + "get_prompt_sections", + "register_tools", + "search_wiki", + "setup", + ], + ), } @@ -795,7 +1062,7 @@ def _is_generated_type_ns(ns_path: str) -> bool: def _project_to_dict_aliases(modules: dict) -> None: - for (mod, cls) in _TO_DICT_ALIAS_CLASSES: + for mod, cls in _TO_DICT_ALIAS_CLASSES: methods = modules.get(mod, {}).get("classes", {}).get(cls) if methods is not None and "to_json" in methods and "to_dict" not in methods: methods.append("to_dict") @@ -816,7 +1083,11 @@ def _project_to_dict_aliases(modules: dict) -> None: ], # Serverless-mode detection free function. "signalwire.utils": [ - ("include/signalwire/utils/serverless.hpp", "is_serverless_mode", "is_serverless_mode"), + ( + "include/signalwire/utils/serverless.hpp", + "is_serverless_mode", + "is_serverless_mode", + ), ], # URL validation free function. "signalwire.utils.url_validator": [ @@ -828,20 +1099,32 @@ def _project_to_dict_aliases(modules: dict) -> None: # ``signalwire.core.security.security_utils``. Grep the PascalCase C++ name; # emit the Python snake_case name. "signalwire.core.security.security_utils": [ - ("include/signalwire/security/security_utils.hpp", - "FilterSensitiveHeaders", "filter_sensitive_headers"), + ( + "include/signalwire/security/security_utils.hpp", + "FilterSensitiveHeaders", + "filter_sensitive_headers", + ), ("include/signalwire/security/security_utils.hpp", "RedactUrl", "redact_url"), - ("include/signalwire/security/security_utils.hpp", - "IsValidHostname", "is_valid_hostname"), + ( + "include/signalwire/security/security_utils.hpp", + "IsValidHostname", + "is_valid_hostname", + ), ], # Inbound-webhook signature validation (webhooks.md). C++ exposes these as # PascalCase free functions in ``signalwire::security``; Python keeps them # module-level under ``signalwire.core.security.webhook_validator``. "signalwire.core.security.webhook_validator": [ - ("include/signalwire/security/webhook_validator.hpp", - "ValidateWebhookSignature", "validate_webhook_signature"), - ("include/signalwire/security/webhook_validator.hpp", - "ValidateRequest", "validate_request"), + ( + "include/signalwire/security/webhook_validator.hpp", + "ValidateWebhookSignature", + "validate_webhook_signature", + ), + ( + "include/signalwire/security/webhook_validator.hpp", + "ValidateRequest", + "validate_request", + ), ], # Framework-free webhook-validation decision core (webhooks.md + # HIDDEN_SURFACE_AUDIT Pass 1). C++ ships it as the ``Validate`` free @@ -852,8 +1135,7 @@ def _project_to_dict_aliases(modules: dict) -> None: # NOT match ``ValidateWebhookSignature(`` / ``ValidateRequest(`` (those # have no word-boundary before ``(``), so this surfaces only the core. "signalwire.core.security.webhook_middleware": [ - ("include/signalwire/security/webhook_validator.hpp", - "Validate", "validate"), + ("include/signalwire/security/webhook_validator.hpp", "Validate", "validate"), ], # Top-level ``signalwire/__init__.py`` package helpers. C++ implements them # as free functions in ``namespace signalwire`` (src/signalwire.cpp, @@ -862,8 +1144,16 @@ def _project_to_dict_aliases(modules: dict) -> None: "signalwire": [ ("include/signalwire/signalwire.hpp", "RestClient", "RestClient"), ("include/signalwire/signalwire.hpp", "register_skill", "register_skill"), - ("include/signalwire/signalwire.hpp", "add_skill_directory", "add_skill_directory"), - ("include/signalwire/signalwire.hpp", "list_skills_with_params", "list_skills_with_params"), + ( + "include/signalwire/signalwire.hpp", + "add_skill_directory", + "add_skill_directory", + ), + ( + "include/signalwire/signalwire.hpp", + "list_skills_with_params", + "list_skills_with_params", + ), ("include/signalwire/signalwire.hpp", "list_skills", "list_skills"), ], # SWAIG schema-inference module-level helpers (type_inference.py). C++ @@ -874,21 +1164,36 @@ def _project_to_dict_aliases(modules: dict) -> None: # params-builder — same output tuple, idiomatic input. "signalwire.core.agent.tools.type_inference": [ ("include/signalwire/swaig/type_inference.hpp", "infer_schema", "infer_schema"), - ("include/signalwire/swaig/type_inference.hpp", - "create_typed_handler_wrapper", "create_typed_handler_wrapper"), + ( + "include/signalwire/swaig/type_inference.hpp", + "create_typed_handler_wrapper", + "create_typed_handler_wrapper", + ), ], # Logging-config module-level helpers. C++ implements them as free functions # in ``signalwire::core::logging_config`` (same snake_case names as Python). "signalwire.core.logging_config": [ - ("include/signalwire/core/logging_config.hpp", - "configure_logging", "configure_logging"), + ( + "include/signalwire/core/logging_config.hpp", + "configure_logging", + "configure_logging", + ), ("include/signalwire/core/logging_config.hpp", "get_logger", "get_logger"), - ("include/signalwire/core/logging_config.hpp", - "reset_logging_configuration", "reset_logging_configuration"), - ("include/signalwire/core/logging_config.hpp", - "strip_control_chars", "strip_control_chars"), - ("include/signalwire/core/logging_config.hpp", - "get_execution_mode", "get_execution_mode"), + ( + "include/signalwire/core/logging_config.hpp", + "reset_logging_configuration", + "reset_logging_configuration", + ), + ( + "include/signalwire/core/logging_config.hpp", + "strip_control_chars", + "strip_control_chars", + ), + ( + "include/signalwire/core/logging_config.hpp", + "get_execution_mode", + "get_execution_mode", + ), ], } @@ -920,7 +1225,9 @@ def _scan_skill_methods(repo: Path) -> dict[str, set[str]]: return defined class_re = re.compile(r"\bclass\s+([A-Za-z_]\w*Skill)\b") # method def: `(...) override` or `(...) const override` or `(...) {` - method_re = re.compile(r"\b([a-z_][a-z0-9_]*)\s*\([^;{]*\)\s*(?:const\s*)?(?:override|noexcept|\{)") + method_re = re.compile( + r"\b([a-z_][a-z0-9_]*)\s*\([^;{]*\)\s*(?:const\s*)?(?:override|noexcept|\{)" + ) for cpp in sorted(src.glob("*.cpp")): text = strip_block_comments(cpp.read_text(encoding="utf-8")) # Drop ``//`` line comments too, so a method name mentioned in a doc @@ -936,16 +1243,48 @@ def _scan_skill_methods(repo: Path) -> dict[str, set[str]]: def _project_builtin_skills(modules: dict, repo: Path) -> None: """Project each built-in skill class into its Python-canonical module with the intersection of Python's recorded methods and the methods the C++ class - genuinely has (own-defined | SkillBase-inherited | ctor).""" + genuinely has (own-defined | SkillBase-inherited | ctor). + + ORACLE-GATED, not list-gated. ``SKILL_PROJECTIONS``' per-class ``py_methods`` + lists are HAND-KEPT and therefore go stale the moment the reference moves a + member. They did: the reference made ``SkillBase.get_prompt_sections()`` a + final template method that applies the ``skip_prompt`` guard and delegates to + a PROTECTED ``_get_prompt_sections()`` hook, so the public member now exists + on the BASE ONLY while every subclass overrides the protected one. The oracle + dropped the public member from 11 skills; the hand list still named it, so + the projection kept emitting public surface the reference does not expose — + 10 phantom ``missing-reference`` additions. + + So the hand list is now an UPPER BOUND intersected with what the surface + oracle LIVE records for that class. A member the reference stopped exposing + stops being projected on the next regen, with no hand edit. This is the same + discipline the signature enumerator's hook projection uses, for the same + reason: a member is emitted only when the C++ class genuinely has it AND the + reference genuinely records it. + + Fail-safe: if the oracle cannot be resolved, fall back to the hand list + rather than silently emitting an EMPTY class surface (which would read as a + mass deletion of real port members). + """ + ref_modules = _load_reference_surface().get("modules", {}) defined = _scan_skill_methods(repo) for cpp_cls, (mod, py_cls, py_methods) in SKILL_PROJECTIONS.items(): if cpp_cls not in defined: continue # skill not implemented in this tree — don't invent it own = defined[cpp_cls] present = [] - for m in py_methods: - if m == "__init__" or m in own or m in _SKILL_BASE_METHODS: - present.append(m) + present.extend( + m + for m in py_methods + if m == "__init__" or m in own or m in _SKILL_BASE_METHODS + ) + # Intersect with the LIVE surface oracle (see docstring). ``__init__`` is + # exempt: it is construction shape, recorded separately. + if ref_modules: + ref_members = set( + ref_modules.get(mod, {}).get("classes", {}).get(py_cls, []) or [] + ) + present = [m for m in present if m == "__init__" or m in ref_members] mod_entry = modules.setdefault(mod, {"classes": {}, "functions": []}) mod_entry["classes"][py_cls] = sorted(set(present)) @@ -997,14 +1336,28 @@ def _project_builtin_skills(modules: dict, repo: Path) -> None: # Every symbol is verified present in the header before it is emitted (abort-loud # on a missing one) so the projection can never invent surface the port lost. _AI_CHAT_CLIENT_METHODS = [ - "__aenter__", "__aexit__", "__init__", "chat", "close", "create_conversation", - "delete", "end", "log", "summarize", "url", + "__aenter__", + "__aexit__", + "__init__", + "chat", + "close", + "create_conversation", + "delete", + "end", + "log", + "summarize", + "url", ] # Method-less classes the oracle records in ai_chat.client: the base error # carries __init__; every error subclass + result struct is bare. _AI_CHAT_EMPTY_CLASSES = [ - "AuthenticationError", "ChatInProgressError", "ChatLog", "ChatResponse", - "ConversationInfo", "ConversationNotFoundError", "RateLimitError", + "AuthenticationError", + "ChatInProgressError", + "ChatLog", + "ChatResponse", + "ConversationInfo", + "ConversationNotFoundError", + "RateLimitError", "SummaryError", ] @@ -1023,7 +1376,8 @@ def _require(pattern: str, what: str) -> None: raise SystemExit( f"enumerate_surface: AI-Chat projection expected {what} in " f"{client_hpp.name} but it is gone -- fix the projection, do not " - f"emit a symbol the port no longer has") + f"emit a symbol the port no longer has" + ) # The client class + the RAII lifecycle members the close/enter/exit fold # relies on (a public ctor and a declared destructor) must genuinely exist. @@ -1042,19 +1396,24 @@ def _require(pattern: str, what: str) -> None: # The base error + every typed subclass and result struct. _require(r"\bclass\s+AIChatError\b", "class AIChatError") _require(r"\bint\s+code\s*\(\s*\)\s*const", "AIChatError::code") - _require(r"\bserver_message\s*\(\s*\)\s*const", - "AIChatError::server_message (reference ``message``)") + _require( + r"\bserver_message\s*\(\s*\)\s*const", + "AIChatError::server_message (reference ``message``)", + ) for _c in _AI_CHAT_EMPTY_CLASSES: kind = r"class" if _c.endswith("Error") else r"struct" _require(rf"\b{kind}\s+{_c}\b", f"{kind} {_c}") # Drop the mis-routed native modules, then emit the single canonical one. - for _native in ("signalwire.ai_chat.ai_chat_client", - "signalwire.ai_chat.ai_chat_error"): + for _native in ( + "signalwire.ai_chat.ai_chat_client", + "signalwire.ai_chat.ai_chat_error", + ): modules.pop(_native, None) client_mod = modules.setdefault( - "signalwire.ai_chat.client", {"classes": {}, "functions": []}) + "signalwire.ai_chat.client", {"classes": {}, "functions": []} + ) client_mod["classes"]["AIChatClient"] = sorted(_AI_CHAT_CLIENT_METHODS) # ``server_message()`` is the C++ spelling of the reference's ``message`` # attribute — ``message`` alone would collide with std::runtime_error::what() @@ -1131,12 +1490,12 @@ def strip_block_comments(text: str) -> str: i = 0 n = len(text) while i < n: - if text[i:i + 2] == "/*": + if text[i : i + 2] == "/*": end = text.find("*/", i + 2) if end == -1: break # Preserve newlines inside the comment to keep line numbers sane. - block = text[i:end + 2] + block = text[i : end + 2] out.append("\n" * block.count("\n")) i = end + 2 else: @@ -1212,9 +1571,10 @@ def strip_attributes(line: str) -> str: class Scope: """A nested scope (namespace or class) stacked during parsing.""" - def __init__(self, kind: str, name: str, brace_depth: int, - visibility: str | None = None): - self.kind = kind # "namespace" | "class" | "struct" | "anon" + def __init__( + self, kind: str, name: str, brace_depth: int, visibility: str | None = None + ): + self.kind = kind # "namespace" | "class" | "struct" | "anon" self.name = name self.brace_depth = brace_depth # Visibility applies to class/struct scopes. struct defaults to public. @@ -1270,7 +1630,7 @@ def parse_header(path: Path) -> list[tuple[str, str, list[str], list[str]]]: opens = code_line.count("{") closes = code_line.count("}") # Push one Scope per part, all sharing the same brace_depth - for i, p in enumerate(parts): + for _i, _p in enumerate(parts): # For nested "a::b", only the last part actually opens a brace. # C++ allows "namespace a::b { ... }" with a single pair. # So the first n-1 parts are logical; only the last increments @@ -1301,12 +1661,14 @@ def parse_header(path: Path) -> list[tuple[str, str, list[str], list[str]]]: closes = code_line.count("}") # The enclosing namespace path at the point this class opens. _ns_here = "::".join(s.name for s in scopes if s.kind == "namespace") - scopes.append(Scope( - "struct" if is_struct else "class", - class_name, - brace_depth, - visibility="public" if is_struct else "private", - )) + scopes.append( + Scope( + "struct" if is_struct else "class", + class_name, + brace_depth, + visibility="public" if is_struct else "private", + ) + ) brace_depth += opens - closes # Generated wire-type / payload structs are METHOD-LESS; the # method-detection path below never registers a class with zero @@ -1330,14 +1692,15 @@ def parse_header(path: Path) -> list[tuple[str, str, list[str], list[str]]]: # Anything deeper is inside a function body (local variables like # ``std::lock_guard lock(mutex_);`` mustn't be misread as # methods). - if scopes and scopes[-1].kind in ("class", "struct") and \ - scopes[-1].visibility == "public" and \ - brace_depth == scopes[-1].brace_depth + 1: + if ( + scopes + and scopes[-1].kind in ("class", "struct") + and scopes[-1].visibility == "public" + and brace_depth == scopes[-1].brace_depth + 1 + ): method_name = extract_method_name(code_line, scopes[-1].name) if method_name is not None: - ns_path = "::".join( - s.name for s in scopes if s.kind == "namespace" - ) + ns_path = "::".join(s.name for s in scopes if s.kind == "namespace") # Nested classes: include the outer class name chain, # but for this SDK that's rare; we only emit the immediate # class's methods under its own name. @@ -1360,9 +1723,7 @@ def parse_header(path: Path) -> list[tuple[str, str, list[str], list[str]]]: # separately because fields are oracle-gated downstream. field_name = extract_field_name(code_line) if field_name is not None: - ns_path = "::".join( - s.name for s in scopes if s.kind == "namespace" - ) + ns_path = "::".join(s.name for s in scopes if s.kind == "namespace") class_name = scopes[-1].name emit_field = _METHOD_RENAMES.get(field_name, field_name) fields.setdefault((ns_path, class_name), []).append(emit_field) @@ -1410,9 +1771,21 @@ def parse_header(path: Path) -> list[tuple[str, str, list[str], list[str]]]: # Type-expression keywords that mean the line is a declaration of something # other than a data member (a nested type, an alias, a template). _FIELD_TYPE_REJECT = { - "using", "typedef", "friend", "template", "enum", "struct", "class", - "union", "namespace", "return", "static_assert", "public", "private", - "protected", "operator", + "using", + "typedef", + "friend", + "template", + "enum", + "struct", + "class", + "union", + "namespace", + "return", + "static_assert", + "public", + "private", + "protected", + "operator", } @@ -1497,10 +1870,29 @@ def extract_method_name(code_line: str, class_name: str) -> str | None: return None # Skip control-flow / reserved words matched as "name" - if name in {"if", "else", "for", "while", "do", "switch", "case", - "return", "sizeof", "throw", "new", "delete", - "typedef", "using", "template", "friend", - "enum", "union", "struct", "class", "namespace"}: + if name in { + "if", + "else", + "for", + "while", + "do", + "switch", + "case", + "return", + "sizeof", + "throw", + "new", + "delete", + "typedef", + "using", + "template", + "friend", + "enum", + "union", + "struct", + "class", + "namespace", + }: return None # Skip operator overloads @@ -1549,6 +1941,7 @@ def extract_method_name(code_line: str, class_name: str) -> str | None: # Module-path translation # --------------------------------------------------------------------------- + def native_ns_to_module(ns_path: str) -> str: """Translate ``signalwire::rest`` -> ``signalwire.rest``. @@ -1595,12 +1988,17 @@ def camel_to_snake(name: str) -> str: # Top-level # --------------------------------------------------------------------------- + def git_sha(repo: Path) -> str: try: - return subprocess.check_output( - ["git", "-C", str(repo), "rev-parse", "HEAD"], - stderr=subprocess.DEVNULL, - ).decode().strip() + return ( + subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "HEAD"], + stderr=subprocess.DEVNULL, + ) + .decode() + .strip() + ) except Exception: return "N/A" @@ -1610,8 +2008,9 @@ def _project_generated_rest_methods(modules: dict) -> None: (from the generator's rest_signatures.json) into its surface class list, materialising inherited base CRUD verbs the header walker can't see.""" here = Path(__file__).resolve().parent - gen_dir = (here.parent / "include" / "signalwire" / "rest" / "namespaces" - / "generated") + gen_dir = ( + here.parent / "include" / "signalwire" / "rest" / "namespaces" / "generated" + ) smap_path = gen_dir / "generated_surface_map.json" sc_path = gen_dir / "rest_signatures.json" if not smap_path.is_file() or not sc_path.is_file(): @@ -1639,7 +2038,8 @@ def _project_generated_rest_methods(modules: dict) -> None: if mod is None: raise SystemExit( f"enumerate_surface: sidecar class {cls!r} not in " - f"generated_surface_map.json (regenerate the REST layer)") + f"generated_surface_map.json (regenerate the REST layer)" + ) mod_entry = modules.setdefault(mod, {"classes": {}, "functions": []}) existing = set(mod_entry["classes"].get(cls, [])) existing.add(canon) @@ -1723,8 +2123,11 @@ def _fold_setters(module: str, cls: str, members: list[str]) -> list[str]: ref_members = ref.get("modules", {}).get(module, {}).get("classes", {}).get(cls) allowed: set[str] = set() if ref_members: - allowed = set(ref_members if isinstance(ref_members, list) - else ref_members.get("members", ref_members)) + allowed = set( + ref_members + if isinstance(ref_members, list) + else ref_members.get("members", ref_members) + ) if module in _FAMILY_GATE_MODULES: allowed |= _agentbase_family_members(ref) if not allowed: @@ -1765,8 +2168,11 @@ def _gate_field_members(module: str, cls: str, decl_fields: list[str]) -> list[s ref_members = ref_modules.get(module, {}).get("classes", {}).get(cls) allowed: set[str] = set() if ref_members: - allowed = set(ref_members if isinstance(ref_members, list) - else ref_members.get("members", ref_members)) + allowed = set( + ref_members + if isinstance(ref_members, list) + else ref_members.get("members", ref_members) + ) if module in _FAMILY_GATE_MODULES: allowed |= _agentbase_family_members(ref) return [f for f in decl_fields if f in allowed] @@ -1785,11 +2191,16 @@ def _agentbase_family_members(ref: dict) -> set[str]: ``_fold_agentbase_family`` membership rule.""" out: set[str] = set() for mod, entry in ref.get("modules", {}).items(): - if mod != "signalwire.core.agent_base" and not mod.startswith(_FAMILY_MIXIN_PREFIX): + if mod != "signalwire.core.agent_base" and not mod.startswith( + _FAMILY_MIXIN_PREFIX + ): continue for members in entry.get("classes", {}).values(): - out |= set(members if isinstance(members, list) - else members.get("members", members)) + out |= set( + members + if isinstance(members, list) + else members.get("members", members) + ) return out @@ -1818,8 +2229,11 @@ def _project_gen_payload_members(modules: dict) -> None: ref_members = ref_classes.get(cls) if not ref_members: continue - ref_set = set(ref_members if isinstance(ref_members, list) - else ref_members.get("members", ref_members)) + ref_set = set( + ref_members + if isinstance(ref_members, list) + else ref_members.get("members", ref_members) + ) present = [f for f in fields if f in ref_set] if not present: continue @@ -1849,7 +2263,9 @@ def _project_client_tree_members(modules: dict) -> None: gen_dir = _INCLUDE_ROOT / "signalwire" / "rest" / "namespaces" / "generated" if not gen_dir.is_dir(): return - mod_entry = modules.setdefault(_CLIENT_TREE_MODULE, {"classes": {}, "functions": []}) + mod_entry = modules.setdefault( + _CLIENT_TREE_MODULE, {"classes": {}, "functions": []} + ) for hdr in sorted(gen_dir.glob("*Namespace.hpp")): srctxt = hdr.read_text(encoding="utf-8") m = re.search(r"(?:class|struct) (\w+Namespace)\s*\{(.*?)\n\};", srctxt, re.S) @@ -1859,8 +2275,11 @@ def _project_client_tree_members(modules: dict) -> None: ref_members = ref_classes.get(cls) if not ref_members: continue - ref_set = set(ref_members if isinstance(ref_members, list) - else ref_members.get("members", ref_members)) + ref_set = set( + ref_members + if isinstance(ref_members, list) + else ref_members.get("members", ref_members) + ) # public data members: `` ;`` at 2-space indent. fields = re.findall(r"^\s{2}([A-Z]\w+)\s+([a-z_]\w*);", body, re.M) present = [mem for _t, mem in fields if mem in ref_set] @@ -1888,7 +2307,8 @@ def _project_client_tree_members(modules: dict) -> None: # parser's ``struct Name {`` regex does not handle. Non-greedy to the matching # ``\n};`` at column 0. _NAMED_STRUCT_RE = re.compile( - r"(?:struct|class)\s+(\w+)\s*(?::[^{]+)?\{(.*?)\n\};", re.S) + r"(?:struct|class)\s+(\w+)\s*(?::[^{]+)?\{(.*?)\n\};", re.S +) def _struct_public_fields(header_txt: str) -> dict[str, list[str]]: @@ -1930,7 +2350,20 @@ def _emit_oracle_gated_fields(modules: dict, module: str, header: Path) -> None: entries, intersected with the fields the reference oracle records for that class. A field surfaces only if the oracle lists it for the same class, so the port's field-idiom folds exactly onto the reference dataclass fields and - never invents surface the reference lacks.""" + never invents surface the reference lacks. + + ``__init__`` folds the same way. The reference spells these classes as + ``@dataclass``es (and, for the credential carriers, as structural fillers with + no source file at all), so their constructor is SYNTHESIZED rather than + written as a ``def`` — porting-sdk 8828dd2 taught the surface oracle to record + it, matching what the signature oracle always did. The C++ counterparts are + aggregates with no user-declared constructor, which is the same contract: + ``std::is_default_constructible`` is true for every one of them (verified for + BasicCredentials/BearerCredentials/RelayEvent/PlayEvent/RequestOptions), and + C++ aggregate-initializes them by field name. So the member is TRUE of the + port, not paperwork to clear a gate. It stays oracle-gated like every other + member here: if the reference does not record a constructor for this class, + the port does not claim one.""" if not header.is_file(): return ref = _load_reference_surface() @@ -1945,9 +2378,14 @@ def _emit_oracle_gated_fields(modules: dict, module: str, header: Path) -> None: ref_members = ref_classes.get(cls) if not ref_members: continue - ref_set = set(ref_members if isinstance(ref_members, list) - else ref_members.get("members", ref_members)) + ref_set = set( + ref_members + if isinstance(ref_members, list) + else ref_members.get("members", ref_members) + ) present = [f for f in fields if f in ref_set] + if "__init__" in ref_set: + present.append("__init__") if not present: continue existing = mod_entry["classes"].get(cls, []) @@ -1974,6 +2412,23 @@ def _project_request_options_fields(modules: dict, repo: Path) -> None: _emit_oracle_gated_fields(modules, "signalwire.rest._request_options", header) +def _project_credential_carrier_fields(modules: dict, repo: Path) -> None: + """Emit the credential carriers' public data-member fields as surface members, + gated on the oracle's ``signalwire.core.auth_handler`` per-class set. + + ``BasicCredentials{username,password}`` and ``BearerCredentials{scheme, + credentials}`` are pure data records: the reference spells them as FastAPI + pydantic models whose whole surface is their fields, so griffe records the + fields and no ``__init__``. The C++ carriers are the same shape — two + ``std::string`` members, zero methods — which is precisely what the regex + method-walker skips (it only registers a class once it sees a public method), + so both classes were absent from ``port_surface.json`` entirely. The oracle + gate is what makes this a fold rather than invented surface: a field appears + only if the reference records it on the same class.""" + header = repo / "include/signalwire/core/auth_handler.hpp" + _emit_oracle_gated_fields(modules, "signalwire.core.auth_handler", header) + + def build_native_names(include_dir: Path) -> dict: """Return the port's REAL declared member names, verbatim, BEFORE any fold. @@ -2006,8 +2461,15 @@ def build_native_names(include_dir: Path) -> dict: for path in header_files: try: findings = parse_header(path) - except Exception: # pragma: no cover — build_snapshot already warns - continue + except Exception as e: + # Same reasoning as build_snapshot: this feeds + # port_surface_native.json, which the doc gates use to resolve + # references. A skipped header makes real symbols look undefined. + raise RuntimeError( + f"enumerate_surface: failed to parse {path}: {e}. " + "Refusing to emit a native-name snapshot that silently omits " + "this header's symbols." + ) from e for _ns_path, class_name, methods, decl_fields in findings: names.add(class_name) names.update(methods) @@ -2032,9 +2494,19 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: for path in header_files: try: findings = parse_header(path) - except Exception as e: # pragma: no cover - print(f"warning: failed to parse {path}: {e}", file=sys.stderr) - continue + except Exception as e: + # ABORT, do not skip. This function produces port_surface.json, which + # the SURFACE-DIFF / DRIFT gates read as ground truth for what the + # port exposes. Swallowing a parse failure silently DROPS every + # symbol in that header, and the gate then reports them as omissions + # the port is missing -- pointing the blame at the port instead of at + # this parser. It used to `print(warning); continue`, which a gate + # that only inspects the exit code cannot see. + raise RuntimeError( + f"enumerate_surface: failed to parse {path}: {e}. " + "Refusing to emit a surface snapshot that silently omits this " + "header's symbols." + ) from e for ns_path, class_name, methods, decl_fields in findings: # Apply class rename (e.g. swml::Service -> SWMLService) @@ -2064,7 +2536,9 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: merged = sorted(set(existing) | set(methods) | set(gated_fields)) # Fold ``set_`` onto ```` where the reference records ```` # on this class — writer/attribute shape idiom (see _fold_setters). - mod_entry["classes"][emit_class] = _fold_setters(emit_mod, emit_class, merged) + mod_entry["classes"][emit_class] = _fold_setters( + emit_mod, emit_class, merged + ) # Apply mixin projections: the C++ AgentBase flattens Python's 9 mixin # classes. Emit the same method list under each mixin module path so @@ -2102,8 +2576,10 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: # manager is as available in C++ as in Python, it is simply already in hand. # Emit it only where the projection actually produced the merged class, so # this can never surface a member for a class the port does not have. - for _mod, _cls in (("signalwire.core.agent.prompt.manager", "PromptManager"), - ("signalwire.core.agent.tools.registry", "ToolRegistry")): + for _mod, _cls in ( + ("signalwire.core.agent.prompt.manager", "PromptManager"), + ("signalwire.core.agent.tools.registry", "ToolRegistry"), + ): _members = modules.get(_mod, {}).get("classes", {}).get(_cls) if _members: modules[_mod]["classes"][_cls] = sorted(set(_members) | {"agent"}) @@ -2120,7 +2596,9 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: # and FabricResourcePUT is a Python-only PUT-marker subclass with no members # (recorded empty). The concrete resources still carry their own method # membership; this only reconciles the shared base layer. - _base = modules.setdefault("signalwire.rest._base", {"classes": {}, "functions": []}) + _base = modules.setdefault( + "signalwire.rest._base", {"classes": {}, "functions": []} + ) for _bcls, _bmeths in ( ("BaseResource", ["__init__"]), ("ReadResource", ["get", "list", "paginate"]), @@ -2175,13 +2653,32 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: # ``signalwire.relay.action``, a PORT_ADDITION) carries these; project the # Python-recorded subset onto relay.call so the base symbol lines up (its # richer C++ surface stays under relay.action as the port addition). - _action_own = modules.get("signalwire.relay.action", {}).get("classes", {}).get("Action", []) + _action_own = ( + modules.get("signalwire.relay.action", {}).get("classes", {}).get("Action", []) + ) if _action_own: # ``control_id`` joins the projected set: it is a ctor param the # reference stores publicly (``self.control_id``), which the oracle's # class-B2 rule now records, and the C++ Action has the accessor. - proj = sorted({"__init__"} | {m for m in ("is_done", "wait", "result", "control_id", "call") - if m in _action_own}) + # ``completed`` likewise: the reference sets ``self.completed = False`` + # in ``__init__`` and flips it True on completion — a caller-observable + # VALUE (class-B2), and the C++ Action already exposes ``completed()`` + # (``is_done()`` is the C++-idiom alias that delegates to it). + proj = sorted( + {"__init__"} + | { + m + for m in ( + "is_done", + "wait", + "result", + "control_id", + "call", + "completed", + ) + if m in _action_own + } + ) modules.setdefault("signalwire.relay.call", {"classes": {}, "functions": []}) modules["signalwire.relay.call"]["classes"]["Action"] = proj # ``call`` is REFERENCE surface (``relay.call.Action.call``), projected @@ -2210,7 +2707,9 @@ def build_snapshot(repo: Path, include_dir: Path) -> dict: re.findall(r"SIGNALWIRE_RELAY_ACTION_SUBCLASS\(([A-Za-z_]\w*)\)", _txt) ) _declared.discard("NAME") # the macro parameter, not a real subclass - call_mod = modules.setdefault("signalwire.relay.call", {"classes": {}, "functions": []}) + call_mod = modules.setdefault( + "signalwire.relay.call", {"classes": {}, "functions": []} + ) for _sub in _declared: _meths = {"__init__"} for _ctl in RELAY_ACTION_CONTROL_METHODS.get(_sub, []): @@ -2265,6 +2764,10 @@ def _ensure_member(mod: str, cls: str, member: str) -> None: # surface members (intersected with the oracle's _request_options set). _project_request_options_fields(modules, repo) + # Credential carriers: project their public data-member fields as surface + # members (intersected with the oracle's signalwire.core.auth_handler set). + _project_credential_carrier_fields(modules, repo) + # Remove empty modules (shouldn't happen in practice but be tidy) modules = {k: v for k, v in modules.items() if v["classes"] or v["functions"]} @@ -2282,19 +2785,25 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - "--include-dir", type=Path, default=default_include, + "--include-dir", + type=Path, + default=default_include, help=f"Header root to walk (default: {default_include})", ) parser.add_argument( - "--output", type=Path, default=default_output, + "--output", + type=Path, + default=default_output, help=f"Where to write JSON (default: {default_output})", ) parser.add_argument( - "--stdout", action="store_true", + "--stdout", + action="store_true", help="Print JSON to stdout instead of writing --output", ) parser.add_argument( - "--check", action="store_true", + "--check", + action="store_true", help="Compare against the file at --output; exit 1 on drift", ) args = parser.parse_args(argv) @@ -2335,7 +2844,8 @@ def strip_meta(s: str) -> str: print(f"error: {native_output} does not exist", file=sys.stderr) return 1 if strip_meta(native_rendered) != strip_meta( - native_output.read_text(encoding="utf-8")): + native_output.read_text(encoding="utf-8") + ): print( "DRIFT: port_surface_native.json is stale relative to headers.\n" " Regenerate:\n" @@ -2350,11 +2860,13 @@ def strip_meta(s: str) -> str: else: args.output.write_text(rendered, encoding="utf-8") native_output.write_text(native_rendered, encoding="utf-8") - print(f"wrote {args.output} " - f"({len(snapshot['modules'])} modules, " - f"{sum(len(m['classes']) for m in snapshot['modules'].values())} classes, " - f"{sum(sum(len(ms) for ms in m['classes'].values()) for m in snapshot['modules'].values())} methods)", - file=sys.stderr) + print( + f"wrote {args.output} " + f"({len(snapshot['modules'])} modules, " + f"{sum(len(m['classes']) for m in snapshot['modules'].values())} classes, " + f"{sum(sum(len(ms) for ms in m['classes'].values()) for m in snapshot['modules'].values())} methods)", + file=sys.stderr, + ) return 0 diff --git a/scripts/generate_relay_protocol.py b/scripts/generate_relay_protocol.py index a2dcd78..fc7c253 100644 --- a/scripts/generate_relay_protocol.py +++ b/scripts/generate_relay_protocol.py @@ -5,18 +5,33 @@ ``signalwire.relay.protocol_types_generated`` module — mirroring python's ``generate_relay_protocol`` and ruby's ``generate_relay_protocol.py``. -Source: the canonical porting-sdk ``relay-protocol/*.json`` — one standalone -JSON-Schema file per RELAY WS method+phase, named -``..(params|result).json``. NOT derived from openapi. - -Class name = PascalCase(``x-method``, fallback filename base) + phase suffix: - calling.ai_hold.params.json -> CallingAiHoldParams - signalwire.connect.result.json -> SignalwireConnectResult +Source: the canonical porting-sdk ``combined-specs/relay.yaml``, read through the +shared reader ``porting-sdk/scripts/relay_protocol_shapes.py`` (ledger row R11). +That reader serves ``{method: schema_node}`` per phase, merging the shapes carried +on a registered method (``methods..request.params_dto`` / +``.response.result``) with the six per phase the extractor found for methods the +vendored spec does not register (``_shapes_unattached.methods.``) — +64 methods per phase either way. NOT derived from openapi. + +This replaced a directory of standalone per-method JSON-Schema files +(``relay-protocol/..(params|result).json``). The method name now +comes from the document's own key rather than from an ``x-method`` field with a +filename fallback, and the phase from the block it was carried in rather than from +a filename suffix. + +Class name = PascalCase(method identifier) + phase suffix: + calling.ai_hold (params phase) -> CallingAiHoldParams + signalwire.connect (result phase) -> SignalwireConnectResult Emit/drop rule = the shared ``is_object_schema`` test: an OBJECT schema WITH properties -> a method-less C++ data struct; empty-object / scalar / union -placeholder -> NOT surfaced. 126 params/result files - 3 empty-object -placeholders = 123 == the oracle exactly (0/0). +placeholder -> NOT surfaced. 64 params shapes less 2 property-less placeholders += 62, 64 result less 3 = 61; 62 + 61 = 123 == the oracle exactly (0/0). + +(The combined document omits the ``type: object`` the per-file envelope used to +declare; ``is_object_schema``'s ``(type is None and properties)`` branch covers +that, so the emit verdict is unchanged. Pinned by +``porting-sdk/tests/test_relay_protocol_shapes.py``.) Output: one struct per file under include/signalwire/relay/protocol_types_generated/.hpp @@ -30,19 +45,21 @@ python3 scripts/generate_relay_protocol.py --check # GEN-FRESH: fail if stale python3 scripts/generate_relay_protocol.py --out DIR # scratch: emit into DIR """ + from __future__ import annotations import argparse import importlib.util import re -import json import sys from pathlib import Path def _load_rest_generator(): here = Path(__file__).resolve().parent - spec = importlib.util.spec_from_file_location("generate_rest", here / "generate_rest.py") + spec = importlib.util.spec_from_file_location( + "generate_rest", here / "generate_rest.py" + ) if spec is None or spec.loader is None: # pragma: no cover raise SystemExit("generate_relay_protocol.py: cannot load generate_rest.py") mod = importlib.util.module_from_spec(spec) @@ -73,19 +90,34 @@ def _pascal_method(method: str) -> str: return "".join(w[:1].upper() + w[1:] for w in parts) +def _load_relay_shapes(psdk: Path): + """The shared porting-sdk reader for ``combined-specs/relay.yaml`` (ledger R11). + + Loaded by FILE PATH — the same way this script already loads generate_rest.py — + because porting-sdk is a sibling checkout, not an installed package. + """ + path = psdk / "scripts" / "relay_protocol_shapes.py" + if not path.is_file(): + raise SystemExit( + f"generate_relay_protocol.py: {path} not found (need porting-sdk adjacency)" + ) + spec = importlib.util.spec_from_file_location("relay_protocol_shapes", path) + if spec is None or spec.loader is None: # pragma: no cover + raise SystemExit(f"generate_relay_protocol.py: cannot load {path}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + def build_outputs(psdk: Path) -> dict: - relay_dir = psdk / "relay-protocol" - by_name = {p.name: p for p in relay_dir.glob("*.json")} + RPS = _load_relay_shapes(psdk) + outs: dict = {} emitted_names: set = set() + # Params first, then result — each mapping already ordered by method name. for phase, suffix in _PHASES: - tail = "." + phase + ".json" - for name in sorted(n for n in by_name if n.endswith(tail)): - node = json.loads(by_name[name].read_text()) - if not isinstance(node, dict): - continue - method = node.get("x-method") or name[: -len(tail)] + for method, node in RPS.shapes(psdk, phase).items(): struct = GR.type_name(_pascal_method(method) + suffix) if not GR.is_object_schema(node): continue @@ -94,8 +126,11 @@ def build_outputs(psdk: Path) -> dict: emitted_names.add(struct) fn = "/".join(RELAY_SUBDIR) + f"/{GR.snake(struct)}.hpp" outs[fn] = GR.emit_methodless_struct( - RELAY_NS, struct, node.get("properties") or {}, - f"RELAY method {method!r}, {phase}.", "generate_relay_protocol.py", + RELAY_NS, + struct, + node.get("properties") or {}, + f"RELAY method {method!r}, {phase}.", + "generate_relay_protocol.py", ) return outs @@ -103,15 +138,19 @@ def build_outputs(psdk: Path) -> dict: def main(argv: list) -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--check", action="store_true", help="GEN-FRESH: exit non-zero if stale") + ap.add_argument( + "--check", action="store_true", help="GEN-FRESH: exit non-zero if stale" + ) ap.add_argument("--out", default="", help="scratch: emit into this dir") args = ap.parse_args(argv) psdk = resolve_porting_sdk() outs = build_outputs(psdk) # Only C++ headers are formatted; any .json sidecars are emitted verbatim. - outs = {fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) - for fn, src in outs.items()} + outs = { + fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) + for fn, src in outs.items() + } out_dir = Path(args.out) if args.out else repo_root() / "include" / "signalwire" @@ -129,11 +168,15 @@ def main(argv: list) -> int: if rel not in expected: stale.append(f"{p} (leftover — not in generator output)") if stale: - sys.stderr.write("GEN-FRESH FAIL: %d generated RELAY-protocol file(s) stale:\n" % len(stale)) + sys.stderr.write( + f"GEN-FRESH FAIL: {len(stale)} generated RELAY-protocol file(s) stale:\n" + ) for s in stale: - sys.stderr.write(" - %s\n" % s) + sys.stderr.write(f" - {s}\n") return 1 - print("GEN-FRESH: generated RELAY-protocol files match porting-sdk/relay-protocol/.") + print( + "GEN-FRESH: generated RELAY-protocol files match porting-sdk/combined-specs/relay.yaml." + ) return 0 for fn, src in outs.items(): diff --git a/scripts/generate_rest.py b/scripts/generate_rest.py index 9e7f972..dea4f99 100644 --- a/scripts/generate_rest.py +++ b/scripts/generate_rest.py @@ -37,6 +37,7 @@ python3 scripts/generate_rest.py --dump-classes # print emitted class set python3 scripts/generate_rest.py --dump-paths # print computed base paths """ + from __future__ import annotations import argparse @@ -53,7 +54,7 @@ raise sys.path.insert(0, str(Path(__file__).resolve().parent)) -from _cpp_fmt import format_generated_cpp # noqa: E402 +from _cpp_fmt import format_generated_cpp # --------------------------------------------------------------------------- @@ -88,8 +89,20 @@ # listed here fails loud in discover_spec_dirs(); a listed spec that no longer # exists on disk is simply skipped. _NS_ORDER = [ - "relay-rest", "fabric", "calling", "video", "datasphere", - "logs", "message", "messages", "voice", "fax", "project", "projects", "chat", "pubsub", + "relay-rest", + "fabric", + "calling", + "video", + "datasphere", + "logs", + "message", + "messages", + "voice", + "fax", + "project", + "projects", + "chat", + "pubsub", "swml-webhooks", ] @@ -131,8 +144,9 @@ def discover_specs(psdk: Path) -> tuple[list[str], list[tuple[str, str, str]]]: resource spec PLUS each types-only spec. Order follows _NS_ORDER; a discovered spec missing from _NS_ORDER aborts.""" rest_apis = psdk / "rest-apis" - found = sorted(d.name for d in rest_apis.iterdir() - if (d / "openapi.yaml").is_file()) + found = sorted( + d.name for d in rest_apis.iterdir() if (d / "openapi.yaml").is_file() + ) resource_dirs: set[str] = set() type_dirs: set[str] = set() for name in found: @@ -157,27 +171,108 @@ def discover_specs(psdk: Path) -> tuple[list[str], list[tuple[str, str, str]]]: ) spec_dirs = [n for n in _NS_ORDER if n in resource_dirs] - type_ns = [(n, _ns_pascal(n), n.replace("-", "_")) - for n in _NS_ORDER if n in type_dirs] + type_ns = [ + (n, _ns_pascal(n), n.replace("-", "_")) for n in _NS_ORDER if n in type_dirs + ] return spec_dirs, type_ns + # C++ reserved words (C++17 keywords) that cannot be an identifier. A body/param # field whose sanitised name collides gets a trailing ``_`` (the wire key is # preserved in the emitted body); such renames are REPORTED as adapter renames. CPP_KEYWORDS = { - "alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor", - "bool", "break", "case", "catch", "char", "char8_t", "char16_t", "char32_t", - "class", "compl", "concept", "const", "consteval", "constexpr", "constinit", - "const_cast", "continue", "co_await", "co_return", "co_yield", "decltype", - "default", "delete", "do", "double", "dynamic_cast", "else", "enum", - "explicit", "export", "extern", "false", "float", "for", "friend", "goto", - "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept", - "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", - "protected", "public", "register", "reinterpret_cast", "requires", "return", - "short", "signed", "sizeof", "static", "static_assert", "static_cast", - "struct", "switch", "template", "this", "thread_local", "throw", "true", - "try", "typedef", "typeid", "typename", "union", "unsigned", "using", - "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq", + "alignas", + "alignof", + "and", + "and_eq", + "asm", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char8_t", + "char16_t", + "char32_t", + "class", + "compl", + "concept", + "const", + "consteval", + "constexpr", + "constinit", + "const_cast", + "continue", + "co_await", + "co_return", + "co_yield", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "inline", + "int", + "long", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static", + "static_assert", + "static_cast", + "struct", + "switch", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor", + "xor_eq", } # Renames recorded during a build (field -> sanitised ident), reported at exit. @@ -188,6 +283,7 @@ def discover_specs(psdk: Path) -> tuple[list[str], list[tuple[str, str, str]]]: # Resolution. # --------------------------------------------------------------------------- + def resolve_porting_sdk() -> Path: env = os.environ.get("PORTING_SDK") if env and (Path(env) / "rest-apis").is_dir(): @@ -197,7 +293,9 @@ def resolve_porting_sdk() -> Path: cand = parent.parent / "porting-sdk" if (cand / "rest-apis").is_dir(): return cand.resolve() - raise SystemExit("generate_rest.py: porting-sdk not found (set $PORTING_SDK or clone adjacent)") + raise SystemExit( + "generate_rest.py: porting-sdk not found (set $PORTING_SDK or clone adjacent)" + ) def repo_root() -> Path: @@ -214,26 +312,33 @@ def repo_root() -> Path: # calling/fabric REST projections + the SWML-verb structs). Each rule is (field, scope-or- # None): scope=None matches in every schema; scope="SchemaName" matches only inside the SPEC # schema (the $defs / components.schemas key) of that name — NOT the C++ struct name we emit. -_overlay_cache: "dict[str, set[tuple[str, str | None]]] | None" = None +_overlay_cache: dict[str, set[tuple[str, str | None]]] | None = None -def _load_overlay() -> "dict[str, set[tuple[str, str | None]]]": +def _load_overlay() -> dict[str, set[tuple[str, str | None]]]: global _overlay_cache if _overlay_cache is None: - def rules(key: str, data: dict) -> "set[tuple[str, str | None]]": + + def rules(key: str, data: dict) -> set[tuple[str, str | None]]: out: set[tuple[str, str | None]] = set() for entry in data.get(key) or []: if isinstance(entry, dict) and entry.get("field"): out.add((entry["field"], entry.get("scope"))) return out + path = resolve_porting_sdk() / "rest-apis" / "x-sdk-overlay.yaml" data = yaml.safe_load(path.read_text()) if path.is_file() else {} data = data or {} - _overlay_cache = {"hidden": rules("hidden", data), "deprecated": rules("deprecated", data)} + _overlay_cache = { + "hidden": rules("hidden", data), + "deprecated": rules("deprecated", data), + } return _overlay_cache -def _overlay_match(rules: "set[tuple[str, str | None]]", field: str, schema_name: "str | None") -> bool: +def _overlay_match( + rules: set[tuple[str, str | None]], field: str, schema_name: str | None +) -> bool: # A rule matches when its field equals `field` AND (it is unscoped OR its scope equals # the containing SPEC schema name). `schema_name` is the schema's name as it appears in # the spec (the $defs / components.schemas key) — NOT the C++ struct name we later emit — @@ -244,11 +349,11 @@ def _overlay_match(rules: "set[tuple[str, str | None]]", field: str, schema_name return False -def _overlay_hidden(field: str, schema_name: "str | None" = None) -> bool: +def _overlay_hidden(field: str, schema_name: str | None = None) -> bool: return _overlay_match(_load_overlay()["hidden"], field, schema_name) -def _overlay_deprecated(field: str, schema_name: "str | None" = None) -> bool: +def _overlay_deprecated(field: str, schema_name: str | None = None) -> bool: return _overlay_match(_load_overlay()["deprecated"], field, schema_name) @@ -256,12 +361,13 @@ def _overlay_deprecated(field: str, schema_name: "str | None" = None) -> bool: # Base loading (x-sdk-bases; §2). # --------------------------------------------------------------------------- + def load_bases(psdk: Path) -> dict[str, list[str]]: raw = yaml.safe_load((psdk / "rest-apis" / "x-sdk-bases.yaml").read_text()) bases = dict(raw.get("x-sdk-bases") or {}) fab = psdk / "rest-apis" / "fabric" / "x-sdk-bases.yaml" if fab.is_file(): - bases.update((yaml.safe_load(fab.read_text()).get("x-sdk-bases") or {})) + bases.update(yaml.safe_load(fab.read_text()).get("x-sdk-bases") or {}) def resolve(name: str, seen: set[str]) -> list[str]: if name in seen: @@ -283,13 +389,16 @@ def resolve(name: str, seen: set[str]) -> list[str]: # Spec model. # --------------------------------------------------------------------------- + class Spec: def __init__(self, name: str, doc: dict): self.name = name self.doc = doc self.server_path = _url_path(doc["servers"][0]["url"]) if self.server_path != "/" and self.server_path.endswith("/"): - raise SystemExit(f"{name}: servers[0].url path {self.server_path!r} has a trailing slash") + raise SystemExit( + f"{name}: servers[0].url path {self.server_path!r} has a trailing slash" + ) self.namespace_attr = (doc.get("x-sdk-namespace") or {}).get("attr") or "" self.ops: dict[str, tuple[str, str, bool]] = {} self.op_body: dict[str, dict] = {} @@ -297,10 +406,16 @@ def __init__(self, name: str, doc: dict): for verb in ("get", "post", "put", "patch", "delete"): o = item.get(verb) if o and o.get("operationId"): - self.ops[o["operationId"]] = (verb, path, bool(o.get("requestBody"))) + self.ops[o["operationId"]] = ( + verb, + path, + bool(o.get("requestBody")), + ) body = o.get("requestBody") or {} content = body.get("content") or {} - media = content.get("application/json") or (next(iter(content.values())) if content else {}) + media = content.get("application/json") or ( + next(iter(content.values())) if content else {} + ) self.op_body[o["operationId"]] = (media or {}).get("schema") or {} self.schemas = ((doc.get("components") or {}).get("schemas")) or {} @@ -321,13 +436,16 @@ def _url_path(url: str) -> str: def load_spec(psdk: Path, ns: str) -> Spec: - return Spec(ns, yaml.safe_load((psdk / "rest-apis" / ns / "openapi.yaml").read_text())) + return Spec( + ns, yaml.safe_load((psdk / "rest-apis" / ns / "openapi.yaml").read_text()) + ) # --------------------------------------------------------------------------- # Path composition (§4). # --------------------------------------------------------------------------- + def join_path(a: str, b: str) -> str: if not b: return a @@ -353,7 +471,7 @@ def relative_tail(spec: Spec, anchor: str, markup: dict, op_path: str): full = join_path(spec.server_path, coll) absp = join_path(spec.server_path, op_path) if coll and absp.startswith(full + "/"): - return ([s for s in absp[len(full) + 1:].split("/") if s], False) + return ([s for s in absp[len(full) + 1 :].split("/") if s], False) if coll and absp == full: return ([], False) return ([s for s in absp.lstrip("/").split("/") if s], True) @@ -363,6 +481,7 @@ def relative_tail(spec: Spec, anchor: str, markup: dict, op_path: str): # Naming. # --------------------------------------------------------------------------- + def snake_to_camel(snake: str) -> str: parts = [p for p in snake.replace("-", "_").replace(".", "_").split("_") if p] if not parts: @@ -465,20 +584,25 @@ def cpp_str(s: str) -> str: # Command-dispatch (§6). # --------------------------------------------------------------------------- + def command_method_name(cmd: str) -> str: s = cmd if s.startswith("calling."): - s = s[len("calling."):] + s = s[len("calling.") :] return s.replace(".", "_") def discriminator_mapping(spec: Spec, schema_name: str) -> list[str]: sch = spec.schemas.get(schema_name) if sch is None: - raise SystemExit(f"command-dispatch request {schema_name!r} not in components.schemas") + raise SystemExit( + f"command-dispatch request {schema_name!r} not in components.schemas" + ) mapping = (sch.get("discriminator") or {}).get("mapping") if not mapping: - raise SystemExit(f"command-dispatch request {schema_name!r} has no discriminator.mapping") + raise SystemExit( + f"command-dispatch request {schema_name!r} has no discriminator.mapping" + ) return list(mapping.keys()) @@ -486,6 +610,7 @@ def discriminator_mapping(spec: Spec, schema_name: str) -> list[str]: # Typed inputs (§5) — schema → C++ native type. # --------------------------------------------------------------------------- + def resolve_schema(spec: Spec, schema: dict | None, seen=None) -> dict: if not schema: return {} @@ -501,7 +626,12 @@ def resolve_schema(spec: Spec, schema: dict | None, seen=None) -> dict: seen.add(leaf) return resolve_schema(spec, spec.schemas.get(leaf), seen) allof = schema.get("allOf") - if allof and len(allof) == 1 and not schema.get("properties") and not schema.get("type"): + if ( + allof + and len(allof) == 1 + and not schema.get("properties") + and not schema.get("type") + ): return resolve_schema(spec, allof[0], seen) return schema @@ -515,7 +645,12 @@ def _json_type(schema: dict) -> str | None: # Distinct int/double — C++ is a typed-numeric language (NO numeric-monotype). -_SCALAR_CPP = {"string": "std::string", "integer": "int", "number": "double", "boolean": "bool"} +_SCALAR_CPP = { + "string": "std::string", + "integer": "int", + "number": "double", + "boolean": "bool", +} def cpp_field_type(spec: Spec, schema: dict) -> str: @@ -541,7 +676,9 @@ def object_body_fields(spec: Spec, body_schema: dict) -> list[tuple[str, dict, b return [(name, psc, name in required) for name, psc in props.items()] -def command_param_fields(spec: Spec, command_schema: dict) -> tuple[list[tuple[str, dict, bool]], bool]: +def command_param_fields( + spec: Spec, command_schema: dict +) -> tuple[list[tuple[str, dict, bool]], bool]: """§6 union-flatten: return ([(wire_name, schema, required)], has_id).""" cs = resolve_schema(spec, command_schema) has_id = "id" in (cs.get("properties") or {}) @@ -579,7 +716,9 @@ def is_object_body(spec: Spec, body_schema: dict) -> bool: return _json_type(resolved) == "object" -def ordered_fields(fields: list[tuple[str, dict, bool]]) -> list[tuple[str, dict, bool]]: +def ordered_fields( + fields: list[tuple[str, dict, bool]], +) -> list[tuple[str, dict, bool]]: req = [f for f in fields if f[2]] opt = [f for f in fields if not f[2]] return req + opt @@ -599,8 +738,10 @@ def _canon_type(spec: Spec, schema: dict, required: bool) -> str: return "optional" resolved = resolve_schema(spec, schema) if schema.get("$ref") or ( - schema.get("allOf") and len(schema.get("allOf")) == 1 - and not schema.get("properties") and not schema.get("type") + schema.get("allOf") + and len(schema.get("allOf")) == 1 + and not schema.get("properties") + and not schema.get("type") ): return "dict" jt = _json_type(resolved) @@ -621,14 +762,21 @@ def _canon_type(spec: Spec, schema: dict, required: bool) -> str: # write-verb ``kwargs`` forward-compat door — sits AFTER it and is ignored as an # optional trailing extra). Kind ``keyword`` matches the reference's keyword-only # slot; type is the concrete RequestOptions class (not a bare ``any``). -_REQUEST_OPTIONS_TYPE = "optional" +_REQUEST_OPTIONS_TYPE = ( + "optional" +) _RO_SIG = "const RequestOptions& request_options = {}" _RO_ARG = "request_options" def _ro_record() -> dict: - return {"name": "request_options", "kind": "keyword", - "type": _REQUEST_OPTIONS_TYPE, "required": False, "default": None} + return { + "name": "request_options", + "kind": "keyword", + "type": _REQUEST_OPTIONS_TYPE, + "required": False, + "default": None, + } def _register_sidecar(cls: str, method: str, records: list[dict]) -> None: @@ -639,6 +787,7 @@ def _register_sidecar(cls: str, method: str, records: list[dict]) -> None: # Emitters — C++ options-struct + method. # --------------------------------------------------------------------------- + def _indent(src: str, pad: str) -> str: """Indent every non-empty line of a multi-line source block by ``pad``.""" return "\n".join((pad + ln) if ln else ln for ln in src.split("\n")) @@ -667,7 +816,10 @@ def gen_header(desc: str, extra_includes: list[str] | None = None) -> str: ] # Quoted (local project) includes form a single clang-format group, sorted # alphabetically — emit them pre-sorted so the formatter is a no-op. - quoted = ['#include "signalwire/rest/base_resource.hpp"'] + list(extra_includes or []) + quoted = [ + '#include "signalwire/rest/base_resource.hpp"', + *list(extra_includes or []), + ] lines += sorted(quoted) lines += [ "", @@ -679,6 +831,7 @@ def gen_header(desc: str, extra_includes: list[str] | None = None) -> str: ] return "\n".join(lines) + "\n" + GEN_FOOTER = """ } // namespace generated } // namespace rest @@ -686,8 +839,14 @@ def gen_header(desc: str, extra_includes: list[str] | None = None) -> str: """ -def _params_struct(struct_name: str, fields: list[tuple[str, dict, bool]], spec: Spec, - cls: str, method: str, leading_records: list[dict]) -> tuple[str, list[str], list[str]]: +def _params_struct( + struct_name: str, + fields: list[tuple[str, dict, bool]], + spec: Spec, + cls: str, + method: str, + leading_records: list[dict], +) -> tuple[str, list[str], list[str]]: """Emit a named options-struct with a member per ordered spec field (required → plain member, optional → std::optional) + a trailing ``json extras`` map. Returns (struct_src, body_build_lines, records).""" @@ -703,11 +862,21 @@ def _params_struct(struct_name: str, fields: list[tuple[str, dict, bool]], spec: if ident != snake_to_camel(wire_name) and ident.rstrip("_") != wire_name: # only record a rename when the identifier truly diverges from wire pass - if ident != wire_name and (wire_name in CPP_KEYWORDS or re.search(r"[^A-Za-z0-9_]", wire_name) or wire_name[:1].isdigit()): + if ident != wire_name and ( + wire_name in CPP_KEYWORDS + or re.search(r"[^A-Za-z0-9_]", wire_name) + or wire_name[:1].isdigit() + ): _RENAMES.append((cls, method, wire_name, ident)) base_t = cpp_field_type(spec, schema) - records.append({"name": wire_name, "kind": "keyword", - "type": _canon_type(spec, schema, required), "required": required}) + records.append( + { + "name": wire_name, + "kind": "keyword", + "type": _canon_type(spec, schema, required), + "required": required, + } + ) if required: lines.append(f" {base_t} {ident};") build.append(f" body[{cpp_str(wire_name)}] = p.{ident};") @@ -718,16 +887,30 @@ def _params_struct(struct_name: str, fields: list[tuple[str, dict, bool]], spec: build.append(" }") # forward-compat door + kwargs sidecar record (kwargs has no distinct member). lines.append(" json extras = json::object();") - records.append({"name": "extras", "kind": "keyword", - "type": "optional>", "required": False, "default": None}) + records.append( + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": False, + "default": None, + } + ) # request_options is the reference's trailing keyword-only param — record it # BEFORE the port-only ``kwargs`` forward-compat door so it aligns with the # oracle position (kwargs then sits after it as the ignored trailing extra). # The C++ ``request_options`` param itself is a separate method param, not a # struct member, so there is no struct-field / build line for it here. records.append(_ro_record()) - records.append({"name": "kwargs", "kind": "var_keyword", "type": "any", - "required": False, "default": {}}) + records.append( + { + "name": "kwargs", + "kind": "var_keyword", + "type": "any", + "required": False, + "default": {}, + } + ) build.append(" if (!p.extras.is_null()) {") build.append(" body.update(p.extras);") build.append(" }") @@ -795,12 +978,19 @@ def abs_cpp_path(full: str, id_args: list[str]) -> str: return " + ".join(parts) if parts else "std::string()" -def _verb_call(recv: str, verb: str, path_expr: str, body_arg: str | None, - query_arg: str | None) -> str: +def _verb_call( + recv: str, verb: str, path_expr: str, body_arg: str | None, query_arg: str | None +) -> str: # Every verb forwards ``request_options`` to the HTTP layer as the trailing # arg — the base HttpClient verbs (get/post/put/patch/del) all take it. It is # transport-only: never written into the wire body/query (EMISSION-neutral). - fn = {"post": "post", "put": "put", "patch": "patch", "get": "get", "delete": "del"}[verb] + fn = { + "post": "post", + "put": "put", + "patch": "patch", + "get": "get", + "delete": "del", + }[verb] if verb == "get": return f"return {recv}.{fn}({path_expr}, {query_arg}, {_RO_ARG});" if verb == "delete": @@ -808,8 +998,9 @@ def _verb_call(recv: str, verb: str, path_expr: str, body_arg: str | None, return f"return {recv}.{fn}({path_expr}, {body_arg}, {_RO_ARG});" -def emit_method(spec: Spec, anchor: str, markup: dict, base: str, - method_snake: str, op_id: str) -> tuple[list[str], list[str]]: +def emit_method( + spec: Spec, anchor: str, markup: dict, base: str, method_snake: str, op_id: str +) -> tuple[list[str], list[str]]: """Return (struct_defs, method_lines) for one declared method.""" if op_id not in spec.ops: raise SystemExit(f"{markup['name']}.{method_snake}: op {op_id!r} not in spec") @@ -822,8 +1013,10 @@ def emit_method(spec: Spec, anchor: str, markup: dict, base: str, # base subclasses receive ``client_`` (protected member of the base). recv = "client_" - id_records = [{"name": a, "kind": "positional", "type": "string", "required": True} - for a in id_args] + id_records = [ + {"name": a, "kind": "positional", "type": "string", "required": True} + for a in id_args + ] id_params = ["const std::string& " + a for a in id_args] write_verb = verb in ("post", "put", "patch") structs: list[str] = [] @@ -836,25 +1029,38 @@ def emit_method(spec: Spec, anchor: str, markup: dict, base: str, struct_name = f"{snake_to_pascal(name)}Params" # _params_struct already threads request_options into the sidecar # records (before the kwargs door); add the C++ param to the signature. - struct_src, build, _ = _params_struct(struct_name, fields, spec, cls, name, id_records) + struct_src, build, _ = _params_struct( + struct_name, fields, spec, cls, name, id_records + ) structs.append(struct_src) - sig = ", ".join(id_params + [f"const {struct_name}& p", _RO_SIG]) + sig = ", ".join([*id_params, f"const {struct_name}& p", _RO_SIG]) lines.append(f" [[nodiscard]] json {name}({sig}) const {{") lines.extend(" " + b for b in build) lines.append(" " + _verb_call(recv, verb, path_expr, "body", None)) lines.append(" }") else: # §5.2 union body → a single positional ``json body`` param. - _register_sidecar(cls, name, id_records + [ - {"name": "body", "kind": "positional", "type": "dict", "required": True}, - _ro_record()]) - sig = ", ".join(id_params + ["const json& body", _RO_SIG]) + _register_sidecar( + cls, + name, + [ + *id_records, + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": True, + }, + _ro_record(), + ], + ) + sig = ", ".join([*id_params, "const json& body", _RO_SIG]) lines.append(f" [[nodiscard]] json {name}({sig}) const {{") lines.append(" " + _verb_call(recv, verb, path_expr, "body", None)) lines.append(" }") elif write_verb: - _register_sidecar(cls, name, id_records + [_ro_record()]) - sig = ", ".join(id_params + [_RO_SIG]) + _register_sidecar(cls, name, [*id_records, _ro_record()]) + sig = ", ".join([*id_params, _RO_SIG]) lines.append(f" [[nodiscard]] json {name}({sig}) const {{") lines.append(" " + _verb_call(recv, verb, path_expr, "json::object()", None)) lines.append(" }") @@ -862,16 +1068,34 @@ def emit_method(spec: Spec, anchor: str, markup: dict, base: str, # §5.3 GET query door — a trailing var_keyword ``params`` map. request_options # records at the reference position (before ``params``); the C++ signature # keeps the ergonomic order (params then request_options, both defaulted). - _register_sidecar(cls, name, id_records + [ - _ro_record(), - {"name": "params", "kind": "var_keyword", "type": "any", "required": False, "default": {}}]) - sig = ", ".join(id_params + ["const std::map& params = {}", _RO_SIG]) + _register_sidecar( + cls, + name, + [ + *id_records, + _ro_record(), + { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": False, + "default": {}, + }, + ], + ) + sig = ", ".join( + [ + *id_params, + "const std::map& params = {}", + _RO_SIG, + ] + ) lines.append(f" [[nodiscard]] json {name}({sig}) const {{") lines.append(" " + _verb_call(recv, verb, path_expr, None, "params")) lines.append(" }") else: # delete - _register_sidecar(cls, name, id_records + [_ro_record()]) - sig = ", ".join(id_params + [_RO_SIG]) + _register_sidecar(cls, name, [*id_records, _ro_record()]) + sig = ", ".join([*id_params, _RO_SIG]) lines.append(f" [[nodiscard]] json {name}({sig}) const {{") lines.append(" " + _verb_call(recv, verb, path_expr, None, None)) lines.append(" }") @@ -882,6 +1106,7 @@ def emit_method(spec: Spec, anchor: str, markup: dict, base: str, # set_methods (§7) support. # --------------------------------------------------------------------------- + def schema_fields(spec: Spec, schema: dict, seen=None) -> set[str]: if schema is None: return set() @@ -934,8 +1159,14 @@ def update_field_schemas(spec: Spec, anchor: str, markup: dict) -> dict[str, dic return {name: psc for name, psc, _ in object_body_fields(spec, sch)} -def emit_set_method(spec: Spec, markup: dict, sm_name: str, sm: dict, - upd_fields: set[str], field_schemas: dict[str, dict]) -> tuple[list[str], list[str]]: +def emit_set_method( + spec: Spec, + markup: dict, + sm_name: str, + sm: dict, + upd_fields: set[str], + field_schemas: dict[str, dict], +) -> tuple[list[str], list[str]]: handler = sm.get("handler") if not handler: raise SystemExit(f"{markup['name']}.{sm_name}: set_method missing handler") @@ -947,24 +1178,45 @@ def emit_set_method(spec: Spec, markup: dict, sm_name: str, sm: dict, struct_name = f"{snake_to_pascal(sm_name)}Params" struct_fields: list[tuple[str, dict, bool]] = [] records: list[dict] = [ - {"name": "resource_id", "kind": "positional", "type": "string", "required": True}] + { + "name": "resource_id", + "kind": "positional", + "type": "string", + "required": True, + } + ] for arg_name, arg in args.items(): field = arg.get("field") if not field: - raise SystemExit(f"{markup['name']}.{sm_name}: arg {arg_name!r} missing field") + raise SystemExit( + f"{markup['name']}.{sm_name}: arg {arg_name!r} missing field" + ) if field not in upd_fields: raise SystemExit( - f"{markup['name']}.{sm_name}: arg field {field!r} not in update request schema") + f"{markup['name']}.{sm_name}: arg field {field!r} not in update request schema" + ) required = bool(arg.get("required")) struct_fields.append((arg_name, field_schemas.get(field, {}), required)) - records.append({"name": arg_name, "kind": "positional", - "type": _canon_type(spec, field_schemas.get(field, {}), required), - "required": required}) + records.append( + { + "name": arg_name, + "kind": "positional", + "type": _canon_type(spec, field_schemas.get(field, {}), required), + "required": required, + } + ) # request_options at the reference position (before the port-only ``extra`` # var_keyword door, which then sits after it as the ignored trailing extra). records.append(_ro_record()) - records.append({"name": "extra", "kind": "var_keyword", "type": "any", - "required": False, "default": {}}) + records.append( + { + "name": "extra", + "kind": "var_keyword", + "type": "any", + "required": False, + "default": {}, + } + ) _register_sidecar(cls, name, records) # Options-struct for the set-method args (member per arg + extras). @@ -981,14 +1233,18 @@ def emit_set_method(spec: Spec, markup: dict, sm_name: str, sm: dict, build.append(f" body[{cpp_str(field)}] = p.{ident};") else: slines.append(f" std::optional<{base_t}> {ident};") - build.append(f" if (p.{ident}.has_value()) {{ body[{cpp_str(field)}] = *p.{ident}; }}") + build.append( + f" if (p.{ident}.has_value()) {{ body[{cpp_str(field)}] = *p.{ident}; }}" + ) slines.append(" json extra = json::object();") slines.append("};") build.append(" if (!p.extra.is_null()) { body.update(p.extra); }") structs.append("\n".join(slines)) - lines = [f" [[nodiscard]] json {name}(const std::string& resource_id, const {struct_name}& p, " - f"{_RO_SIG}) const {{"] + lines = [ + f" [[nodiscard]] json {name}(const std::string& resource_id, const {struct_name}& p, " + f"{_RO_SIG}) const {{" + ] lines.extend(" " + b for b in build) lines.append(f" return update(resource_id, body, {_RO_ARG});") lines.append(" }") @@ -999,6 +1255,7 @@ def emit_set_method(spec: Spec, markup: dict, sm_name: str, sm: dict, # Command-dispatch emitter (§6). # --------------------------------------------------------------------------- + def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: name = markup["name"] request = markup.get("request") @@ -1013,7 +1270,9 @@ def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: structs: list[str] = [] methods: list[str] = [] - mapping = (spec.schemas.get(request).get("discriminator") or {}).get("mapping") or {} + mapping = (spec.schemas.get(request).get("discriminator") or {}).get( + "mapping" + ) or {} for cmd in commands: mname = command_method_name(cmd) cmd_ref = mapping.get(cmd) or "" @@ -1023,30 +1282,55 @@ def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: records: list[dict] = [] if with_id: - records.append({"name": "call_id", "kind": "positional", - "type": "string", "required": True}) + records.append( + { + "name": "call_id", + "kind": "positional", + "type": "string", + "required": True, + } + ) struct_name = f"{snake_to_pascal(mname)}Params" # build options struct slines = [f"struct {struct_name} {{"] build = [" json params = json::object();"] for wire_name, schema, required in ordered_fields(fields): ident = snake_ident(wire_name) - if ident != wire_name and (wire_name in CPP_KEYWORDS or re.search(r"[^A-Za-z0-9_]", wire_name) or wire_name[:1].isdigit()): + if ident != wire_name and ( + wire_name in CPP_KEYWORDS + or re.search(r"[^A-Za-z0-9_]", wire_name) + or wire_name[:1].isdigit() + ): _RENAMES.append((name, mname, wire_name, ident)) base_t = cpp_field_type(spec, schema) - records.append({"name": wire_name, "kind": "keyword", - "type": _canon_type(spec, schema, required), "required": required}) + records.append( + { + "name": wire_name, + "kind": "keyword", + "type": _canon_type(spec, schema, required), + "required": required, + } + ) if required: slines.append(f" {base_t} {ident};") build.append(f" params[{cpp_str(wire_name)}] = p.{ident};") else: slines.append(f" std::optional<{base_t}> {ident};") - build.append(f" if (p.{ident}.has_value()) {{ params[{cpp_str(wire_name)}] = *p.{ident}; }}") + build.append( + f" if (p.{ident}.has_value()) {{ params[{cpp_str(wire_name)}] = *p.{ident}; }}" + ) slines.append(" json extras = json::object();") slines.append("};") build.append(" if (!p.extras.is_null()) { params.update(p.extras); }") - records.append({"name": "extras", "kind": "keyword", - "type": "optional>", "required": False, "default": None}) + records.append( + { + "name": "extras", + "kind": "keyword", + "type": "optional>", + "required": False, + "default": None, + } + ) # request_options is the reference's trailing keyword-only command param — # forwarded to the POST via execute(), never merged into the {command,params} # wire body. @@ -1058,19 +1342,25 @@ def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: # No ``= {}`` default: a nested struct with a default member initializer # cannot be a default argument inside the enclosing class definition # (C++ rule). Callers pass ``{}`` explicitly for the all-optional commands. - sig = ", ".join(id_param + [f"const {struct_name}& p", _RO_SIG]) - call_arg = "call_id" if with_id else "std::nullopt" + sig = ", ".join([*id_param, f"const {struct_name}& p", _RO_SIG]) methods.append(f" [[nodiscard]] json {mname}({sig}) const {{") methods.extend(" " + b for b in build) if with_id: - methods.append(f" return execute({cpp_str(cmd)}, params, call_id, {_RO_ARG});") + methods.append( + f" return execute({cpp_str(cmd)}, params, call_id, {_RO_ARG});" + ) else: - methods.append(f" return execute({cpp_str(cmd)}, params, std::nullopt, {_RO_ARG});") + methods.append( + f" return execute({cpp_str(cmd)}, params, std::nullopt, {_RO_ARG});" + ) methods.append(" }") lines = [] - lines.append(gen_header( - f"Generated command-dispatch resource for the {spec.name!r} namespace.")) + lines.append( + gen_header( + f"Generated command-dispatch resource for the {spec.name!r} namespace." + ) + ) lines.append("") lines.append(f"/// {name} — command-dispatch resource ({spec.name} spec).") lines.append(f"/// Each method POSTs {{command, params, id?}} to {base}.") @@ -1088,11 +1378,13 @@ def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: lines.extend(methods) lines.append("") lines.append(" private:") - lines.append(" [[nodiscard]] json execute(const std::string& command, const json& params, " - "const std::optional& call_id = std::nullopt, " - "const RequestOptions& request_options = {}) const {") - lines.append(" json body = {{\"command\", command}, {\"params\", params}};") - lines.append(" if (call_id.has_value()) { body[\"id\"] = *call_id; }") + lines.append( + " [[nodiscard]] json execute(const std::string& command, const json& params, " + "const std::optional& call_id = std::nullopt, " + "const RequestOptions& request_options = {}) const {" + ) + lines.append(' json body = {{"command", command}, {"params", params}};') + lines.append(' if (call_id.has_value()) { body["id"] = *call_id; }') lines.append(" return http_.post(kBasePath, body, request_options);") lines.append(" }") lines.append("") @@ -1106,6 +1398,7 @@ def emit_command_dispatch(spec: Spec, anchor: str, markup: dict) -> str: # Resource emitter. # --------------------------------------------------------------------------- + def emit_resource(spec: Spec, anchor: str, markup: dict) -> str: name = markup["name"] base = markup["base"] @@ -1119,9 +1412,13 @@ def emit_resource(spec: Spec, anchor: str, markup: dict) -> str: if not upd: raise SystemExit(f"{name}: {base} requires update_method") item = spec.doc["paths"][anchor] - spec_verb = "PUT" if item.get("put") else ("PATCH" if item.get("patch") else None) + spec_verb = ( + "PUT" if item.get("put") else ("PATCH" if item.get("patch") else None) + ) if spec_verb and upd != spec_verb: - raise SystemExit(f"{name}: update_method {upd} != spec update verb {spec_verb}") + raise SystemExit( + f"{name}: update_method {upd} != spec update verb {spec_verb}" + ) extends = EXTENDS[base] bp = base_path(spec, anchor, markup) @@ -1158,7 +1455,9 @@ def emit_resource(spec: Spec, anchor: str, markup: dict) -> str: upd_fields = update_request_fields(spec, anchor, markup) upd_field_schemas = update_field_schemas(spec, anchor, markup) for sm_name, sm in set_methods.items(): - structs, mlines = emit_set_method(spec, markup, sm_name, sm, upd_fields, upd_field_schemas) + structs, mlines = emit_set_method( + spec, markup, sm_name, sm, upd_fields, upd_field_schemas + ) all_structs.extend(structs) if all_methods: all_methods.append("") @@ -1200,8 +1499,13 @@ def emit_resource(spec: Spec, anchor: str, markup: dict) -> str: # ``request_options`` (PY-7/PY-9). It records at the reference's position — right # after the verb's real params — so the port-only query/body extras (``params`` # var_keyword door, the loose ``body``) sit AFTER it as ignored trailing extras. - _params_door = {"name": "params", "kind": "var_keyword", "type": "any", - "required": False, "default": {}} + _params_door = { + "name": "params", + "kind": "var_keyword", + "type": "any", + "required": False, + "default": {}, + } def _verb_records(verb: str) -> list[dict]: if verb == "list": @@ -1213,20 +1517,47 @@ def _verb_records(verb: str) -> list[dict]: # PaginatedIterator class. return [_ro_record(), dict(_params_door)] if verb == "get": - return [{"name": "id", "kind": "positional", "type": "string", "required": True}, - _ro_record(), dict(_params_door)] + return [ + { + "name": "id", + "kind": "positional", + "type": "string", + "required": True, + }, + _ro_record(), + dict(_params_door), + ] if verb == "delete": - return [{"name": "id", "kind": "positional", "type": "string", "required": True}, - _ro_record()] + return [ + { + "name": "id", + "kind": "positional", + "type": "string", + "required": True, + }, + _ro_record(), + ] if verb == "create": - return [_ro_record(), - {"name": "body", "kind": "positional", "type": "dict", - "required": True}] - # update - return [{"name": "id", "kind": "positional", "type": "string", "required": True}, + return [ _ro_record(), - {"name": "body", "kind": "positional", "type": "dict", - "required": True}] + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": True, + }, + ] + # update + return [ + {"name": "id", "kind": "positional", "type": "string", "required": True}, + _ro_record(), + { + "name": "body", + "kind": "positional", + "type": "dict", + "required": True, + }, + ] for verb in _INHERITED_VERBS_BY_BASE.get(base, ()): if (name, verb) in _SIDECAR: @@ -1247,7 +1578,9 @@ def _verb_records(verb: str) -> list[dict]: lines.append("") if base in ("CrudResource", "FabricResource"): lines.append(f" explicit {name}(const HttpClient& client)") - lines.append(f" : {extends}(client, {cpp_str(bp)}, {cpp_str(update_method)}) {{}}") + lines.append( + f" : {extends}(client, {cpp_str(bp)}, {cpp_str(update_method)}) {{}}" + ) else: lines.append(f" explicit {name}(const HttpClient& client)") lines.append(f" : {extends}(client, {cpp_str(bp)}) {{}}") @@ -1273,10 +1606,15 @@ def _verb_records(verb: str) -> list[dict]: } ATTR_OVERRIDE = { - "GenericResources": "resources", "FabricAddresses": "addresses", - "FabricTokens": "tokens", "DatasphereDocuments": "documents", - "ProjectTokens": "tokens", "PubSub": "pubsub", - "MessageLogs": "messages", "VoiceLogs": "voice", "FaxLogs": "fax", + "GenericResources": "resources", + "FabricAddresses": "addresses", + "FabricTokens": "tokens", + "DatasphereDocuments": "documents", + "ProjectTokens": "tokens", + "PubSub": "pubsub", + "MessageLogs": "messages", + "VoiceLogs": "voice", + "FaxLogs": "fax", "ConferenceLogs": "conferences", } @@ -1287,7 +1625,7 @@ def container_accessor(markup: dict, name: str, container: str) -> str: if name in ATTR_OVERRIDE: return ATTR_OVERRIDE[name] lead = container[:1].upper() + container[1:] - stem = name[len(lead):] if name.startswith(lead) else name + stem = name[len(lead) :] if name.startswith(lead) else name # snake_case the pascal stem s = re.sub(r"(? str: cls = CONTAINERS[container] - includes = [f'#include "signalwire/rest/namespaces/generated/{class_name}.hpp"' - for _, class_name in members] - lines = [gen_header( - f"Generated REST client container for the {container} namespace (§8).", includes)] + includes = [ + f'#include "signalwire/rest/namespaces/generated/{class_name}.hpp"' + for _, class_name in members + ] + lines = [ + gen_header( + f"Generated REST client container for the {container} namespace (§8).", + includes, + ) + ] lines.append("") - lines.append(f"/// {cls} — generated container grouping the {container} namespace resources (§8).") + lines.append( + f"/// {cls} — generated container grouping the {container} namespace resources (§8)." + ) lines.append(f"class {cls} {{") lines.append(" public:") lines.append(f" explicit {cls}(const HttpClient& http)") @@ -1336,7 +1682,7 @@ def emit_resource_tree(placed) -> str: flats = [] containers_seen = [] seen_c = set() - for spec, anchor, markup, container in placed: + for _spec, _anchor, markup, container in placed: name = markup["name"] if not container: flats.append((flat_accessor(name), name)) @@ -1345,28 +1691,31 @@ def emit_resource_tree(placed) -> str: seen_c.add(container) containers_seen.append(container) - includes = [f'#include "signalwire/rest/namespaces/generated/{cls}.hpp"' for _, cls in flats] - includes += [f'#include "signalwire/rest/namespaces/generated/{CONTAINERS[c]}.hpp"' - for c in containers_seen] - lines = [gen_header( - "Generated REST resource tree the hand RestClient composes (§8).", includes)] + includes = [ + f'#include "signalwire/rest/namespaces/generated/{cls}.hpp"' for _, cls in flats + ] + includes += [ + f'#include "signalwire/rest/namespaces/generated/{CONTAINERS[c]}.hpp"' + for c in containers_seen + ] + lines = [ + gen_header( + "Generated REST resource tree the hand RestClient composes (§8).", includes + ) + ] lines.append("") lines.append("/// ResourceTree — flat resources plus namespace containers.") lines.append("/// Groups every REST resource under its API namespace so the") lines.append("/// RestClient can expose them as a single accessor tree.") lines.append("struct ResourceTree {") - ctor_inits = [] - for acc, cls in flats: - ctor_inits.append(f"{acc}(http)") - for c in containers_seen: - ctor_inits.append(f"{c}(http)") + ctor_inits = [f"{acc}(http)" for acc, _cls in flats] + ctor_inits.extend(f"{c}(http)" for c in containers_seen) lines.append(" explicit ResourceTree(const HttpClient& http)") lines.append(" : " + ", ".join(ctor_inits) + " {}") lines.append("") for acc, cls in flats: lines.append(f" {cls} {acc};") - for c in containers_seen: - lines.append(f" {CONTAINERS[c]} {c};") + lines.extend(f" {CONTAINERS[c]} {c};" for c in containers_seen) lines.append("};") lines.append(GEN_FOOTER) return "\n".join(lines) @@ -1427,7 +1776,11 @@ def is_object_schema(node: dict) -> bool: return False props = node.get("properties") t = _type_schema_type(node) - return (t == "object" or (t is None and props)) and isinstance(props, dict) and len(props) > 0 + return ( + (t == "object" or (t is None and props)) + and isinstance(props, dict) + and len(props) > 0 + ) def _methodless_field_type(psc: dict) -> str: @@ -1444,9 +1797,14 @@ def _methodless_field_type(psc: dict) -> str: return _SCALAR_CPP.get(t, "json") -def emit_methodless_struct(ns_segments: list[str], name: str, properties: dict, - source_desc: str, regen_cmd: str, - schema_name: "str | None" = None) -> str: +def emit_methodless_struct( + ns_segments: list[str], + name: str, + properties: dict, + source_desc: str, + regen_cmd: str, + schema_name: str | None = None, +) -> str: """Emit one method-less C++ data struct under an arbitrary nested namespace path, carrying one typed member per snake wire key + a trailing ``json extras``. Shared by the REST wire-type emitter and the SWML-verbs / relay-protocol / SWAIG @@ -1476,15 +1834,16 @@ def emit_methodless_struct(ns_segments: list[str], name: str, properties: dict, "#include ", "", ] - for seg in ns_segments: - lines.append(f"namespace {seg} {{") + lines.extend(f"namespace {seg} {{" for seg in ns_segments) lines.append("") lines.append("using json = nlohmann::json;") lines.append("") lines.append(f"/// {name} — generated read-side data type.") lines.append(f"/// {source_desc}") lines.append("///") - lines.append("/// Method-less DTO: one typed member per snake wire key + open `extras`.") + lines.append( + "/// Method-less DTO: one typed member per snake wire key + open `extras`." + ) lines.append(f"struct {name} {{") # Members emitted raw (single space before any trailing `// wire key:` comment); # the shared format_generated_cpp pass aligns trailing comments + reflows the doc @@ -1507,8 +1866,7 @@ def emit_methodless_struct(ns_segments: list[str], name: str, properties: dict, lines.append(" json extras = json::object();") lines.append("};") lines.append("") - for seg in reversed(ns_segments): - lines.append(f"}} // namespace {seg}") + lines.extend(f"}} // namespace {seg}" for seg in reversed(ns_segments)) return "\n".join(lines) + "\n" @@ -1528,7 +1886,9 @@ def _enum_const_name(value: str) -> str: return s -def emit_type_enum(ns_seg: str, enum_name: str, values: list, ns_key: str, raw_name: str) -> str: +def emit_type_enum( + ns_seg: str, enum_name: str, values: list, ns_key: str, raw_name: str +) -> str: """Emit a method-less C++ struct carrying static string constants (value == wire string) grouped into an ``all()`` list — the port's closed-set idiom for an x-sdk-enum public enum. The reference records it method-less; ``all()`` is a @@ -1589,7 +1949,12 @@ def emit_types(psdk: Path, outs: dict, type_ns: list[tuple[str, str, str]]) -> N fn = f"types/{ns_key}/{snake(enum_name)}.hpp" if fn not in outs: outs[fn] = emit_type_enum( - ns_seg, enum_name, list(node.get("enum") or []), ns_key, raw_name) + ns_seg, + enum_name, + list(node.get("enum") or []), + ns_key, + raw_name, + ) continue if is_object_schema(node): struct = type_name(raw_name) @@ -1597,7 +1962,9 @@ def emit_types(psdk: Path, outs: dict, type_ns: list[tuple[str, str, str]]) -> N if fn not in outs: ns_segments = ["signalwire", "rest", "generated", "types", ns_seg] outs[fn] = emit_methodless_struct( - ns_segments, struct, node.get("properties") or {}, + ns_segments, + struct, + node.get("properties") or {}, f"Generated REST wire type for the {ns_key!r} namespace " f"(components/schemas {raw_name!r}).", "generate_rest.py", @@ -1609,6 +1976,7 @@ def emit_types(psdk: Path, outs: dict, type_ns: list[tuple[str, str, str]]) -> N # Driver. # --------------------------------------------------------------------------- + def build_outputs(psdk: Path) -> dict[str, str]: load_bases(psdk) # validate x-sdk-bases (fail loud) _SIDECAR.clear() @@ -1623,7 +1991,7 @@ def build_outputs(psdk: Path) -> dict[str, str]: placed = resolve_placement(specs) by_container: dict[str, list[tuple[str, str]]] = {} order: list[str] = [] - for spec, anchor, markup, container in placed: + for _spec, _anchor, markup, container in placed: if not container: continue if container not in by_container: @@ -1633,7 +2001,9 @@ def build_outputs(psdk: Path) -> dict[str, str]: by_container[container].append((acc, markup["name"])) for container in order: if container not in CONTAINERS: - raise SystemExit(f"container attr {container!r} has no C++ container class (add to CONTAINERS)") + raise SystemExit( + f"container attr {container!r} has no C++ container class (add to CONTAINERS)" + ) cls = CONTAINERS[container] outs[cls + ".hpp"] = emit_container(container, by_container[container]) @@ -1652,30 +2022,38 @@ def build_outputs(psdk: Path) -> dict[str, str]: surface_map: dict[str, str] = {} for spec in specs: module = f"signalwire.rest.namespaces.{spec.name.replace('-', '_')}_resources_generated" - for anchor, markup in spec.resources(): + for _anchor, markup in spec.resources(): surface_map[markup["name"]] = module for container_cls in sorted(set(CONTAINERS.values())): surface_map[container_cls] = "signalwire.rest.namespaces._client_tree_generated" - outs["generated_surface_map.json"] = _json.dumps( - dict(sorted(surface_map.items())), indent=2, - ) + "\n" + outs["generated_surface_map.json"] = ( + _json.dumps( + dict(sorted(surface_map.items())), + indent=2, + ) + + "\n" + ) # Sidecar (§5): canonical typed-param records the signature enumerator unfolds # onto the reflected options-struct params (libclang can't express keyword-only # intent, the json element type, or the open extras dict). sidecar: dict[str, list[dict]] = {} - for (cls, method) in sorted(_SIDECAR.keys()): + for cls, method in sorted(_SIDECAR.keys()): sidecar[f"{cls}::{method}"] = _SIDECAR[(cls, method)] - outs["rest_signatures.json"] = _json.dumps( - { - "_comment": "Code generated by scripts/generate_rest.py; DO NOT EDIT. " - "Canonical typed-param records for generated REST operation/" - "command/set methods; consumed by scripts/enumerate_signatures.py " - "to unfold the reflected C++ options-struct params onto the oracle shape.", - "methods": sidecar, - }, - indent=2, sort_keys=False, - ) + "\n" + outs["rest_signatures.json"] = ( + _json.dumps( + { + "_comment": "Code generated by scripts/generate_rest.py; DO NOT EDIT. " + "Canonical typed-param records for generated REST operation/" + "command/set methods; consumed by scripts/enumerate_signatures.py " + "to unfold the reflected C++ options-struct params onto the oracle shape.", + "methods": sidecar, + }, + indent=2, + sort_keys=False, + ) + + "\n" + ) return outs @@ -1684,7 +2062,7 @@ def _print_classes(psdk: Path) -> None: specs = [load_spec(psdk, ns) for ns in spec_dirs] per_ns: dict[str, list[str]] = {} for spec in specs: - for anchor, markup in spec.resources(): + for _anchor, markup in spec.resources(): ns = spec.name.replace("-", "_") # relay-rest registry resources belong to relay_rest module (namespace: # registry only affects client-tree placement, not the module). @@ -1701,7 +2079,11 @@ def _print_paths(psdk: Path) -> None: for anchor, markup in spec.resources(): if markup.get("kind") == "command-dispatch": op = spec.ops.get("call-commands") - bp = join_path(spec.server_path, op[1].lstrip("/")) if op else join_path(spec.server_path, anchor.lstrip("/")) + bp = ( + join_path(spec.server_path, op[1].lstrip("/")) + if op + else join_path(spec.server_path, anchor.lstrip("/")) + ) else: bp = base_path(spec, anchor, markup) print(f"{markup['name']}\t{bp}") @@ -1709,10 +2091,16 @@ def _print_paths(psdk: Path) -> None: def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--check", action="store_true", help="GEN-FRESH: exit non-zero if stale") + ap.add_argument( + "--check", action="store_true", help="GEN-FRESH: exit non-zero if stale" + ) ap.add_argument("--out", default="", help="scratch: emit flat into this dir") - ap.add_argument("--dump-classes", action="store_true", help="print \\t and exit") - ap.add_argument("--dump-paths", action="store_true", help="print \\t and exit") + ap.add_argument( + "--dump-classes", action="store_true", help="print \\t and exit" + ) + ap.add_argument( + "--dump-paths", action="store_true", help="print \\t and exit" + ) args = ap.parse_args(argv) psdk = resolve_porting_sdk() @@ -1726,13 +2114,17 @@ def main(argv: list[str]) -> int: outs = build_outputs(psdk) # Only C++ headers are formatted; any .json sidecars are emitted verbatim. - outs = {fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) - for fn, src in outs.items()} + outs = { + fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) + for fn, src in outs.items() + } if args.out: out_dir = Path(args.out) else: - out_dir = repo_root() / "include" / "signalwire" / "rest" / "namespaces" / "generated" + out_dir = ( + repo_root() / "include" / "signalwire" / "rest" / "namespaces" / "generated" + ) if args.check: stale = [] @@ -1746,9 +2138,11 @@ def main(argv: list[str]) -> int: if rel not in expected: stale.append(f"{p} (leftover — not in generator output)") if stale: - sys.stderr.write("GEN-FRESH FAIL: %d generated REST file(s) stale:\n" % len(stale)) + sys.stderr.write( + f"GEN-FRESH FAIL: {len(stale)} generated REST file(s) stale:\n" + ) for s in stale: - sys.stderr.write(" - %s\n" % s) + sys.stderr.write(f" - {s}\n") return 1 print("GEN-FRESH: generated REST files match the canonical specs.") return 0 @@ -1760,8 +2154,10 @@ def main(argv: list[str]) -> int: p.write_text(src) print(f"generated {len(outs)} REST file(s) into {out_dir}") if _RENAMES: - print(f"\nADAPTER RENAMES ({len(_RENAMES)}) — reserved-word/non-identifier fields " - "(wire key preserved, param identifier escaped):") + print( + f"\nADAPTER RENAMES ({len(_RENAMES)}) — reserved-word/non-identifier fields " + "(wire key preserved, param identifier escaped):" + ) for cls, method, wire, ident in _RENAMES: print(f" {cls}.{method}: {wire!r} -> {ident}") return 0 diff --git a/scripts/generate_rest_tests.py b/scripts/generate_rest_tests.py index 508cba5..a95ded5 100644 --- a/scripts/generate_rest_tests.py +++ b/scripts/generate_rest_tests.py @@ -27,6 +27,7 @@ --check regenerate to a temp dir and diff against the checked-in files; non-zero exit if they differ (GEN-FRESH-TESTS) """ + from __future__ import annotations import argparse @@ -35,7 +36,6 @@ import re import subprocess import sys -import tempfile from pathlib import Path HERE = Path(__file__).resolve().parent @@ -58,13 +58,24 @@ def _resolve_psdk() -> Path: if not PSDK.is_dir(): raise SystemExit( f"porting-sdk not found at {PSDK}; clone it adjacent to this repo " - "(../porting-sdk) or set PORTING_SDK_DIR") + "(../porting-sdk) or set PORTING_SDK_DIR" + ) # Import the mock's own spec loader so our matched_route resolution is identical # to what the mock journals at runtime. sys.path.insert(0, str(PSDK / "test_harness" / "mock_signalwire")) from mock_signalwire.specs import SpecLoader # type: ignore # noqa: E402 +# The generated tests must be clang-format-clean AT EMIT. Otherwise GEN-FRESH-TESTS +# (byte-compares a fresh regen against the tree) and the FMT gate (--check) are +# MUTUALLY EXCLUSIVE and one of them is always red -- AGENT_RULES §5. We use the +# real clang-format rather than the pure-python subset in format_generated_cpp: +# that subset targets DECLARATIONS and mis-wraps the `(void)(...)` cast these +# templates emit (it breaks the line after the opening paren). Shelling out to the +# same binary the FMT gate runs makes agreement true by construction. +sys.path.insert(0, str(HERE)) +from _cpp_fmt import clang_format_source # type: ignore # noqa: E402 + TESTS_DIR = PORT_ROOT / "tests" GEN_PREFIX = "test_rest_generated_" @@ -78,7 +89,8 @@ def build_and_run_registry() -> dict: build = PORT_ROOT / "build" subprocess.run( ["cmake", "--build", str(build), "--target", "route_registry", "-j", "8"], - check=True, stdout=subprocess.DEVNULL, + check=True, + stdout=subprocess.DEVNULL, ) out = subprocess.run( [str(build / "route_registry")], check=True, capture_output=True, text=True @@ -91,7 +103,9 @@ def load_spec_routes() -> list: return SpecLoader(spec_root=PSDK / "rest-apis").load_all().routes -def resolve_matched_route(method: str, path_template: str, routes: list) -> tuple[str, str] | None: +def resolve_matched_route( + method: str, path_template: str, routes: list +) -> tuple[str, str] | None: """Return (spec_name, endpoint_id) the mock would journal for this dispatch. Mirrors the mock: substitute {id}->CONCRETE, match against every spec route @@ -99,7 +113,8 @@ def resolve_matched_route(method: str, path_template: str, routes: list) -> tupl """ concrete = path_template.replace("{id}", CONCRETE) candidates = [ - r for r in routes + r + for r in routes if r.method == method.upper() and r.match(concrete) is not None ] if not candidates: @@ -194,8 +209,7 @@ def rewrite_call(call: str) -> str: s = re.sub(r"\bSENTINEL\b", '"X"', s) s = re.sub(r"(?{}", s) - return s + return re.sub(r"(?{}", s) def generate(plan: dict, routes: list) -> dict[str, str]: @@ -213,13 +227,21 @@ def generate(plan: dict, routes: list) -> dict[str, str]: if unmapped: for r in unmapped: - print(f"UNMAPPED {r['method']} {r['path_template']} (via {r['via']})", file=sys.stderr) - raise SystemExit(f"generate_rest_tests: {len(unmapped)} route(s) matched no spec operationId") + print( + f"UNMAPPED {r['method']} {r['path_template']} (via {r['via']})", + file=sys.stderr, + ) + raise SystemExit( + f"generate_rest_tests: {len(unmapped)} route(s) matched no spec operationId" + ) out: dict[str, str] = {} for ns in sorted(by_ns): # deterministic order: by (method, path, via) - entries = sorted(by_ns[ns], key=lambda e: (e[0]["method"], e[0]["path_template"], e[0]["via"])) + entries = sorted( + by_ns[ns], + key=lambda e: (e[0]["method"], e[0]["path_template"], e[0]["via"]), + ) parts = [HEADER.format(ns=ns)] for r, endpoint in entries: call = rewrite_call(r["call"]) @@ -228,20 +250,32 @@ def generate(plan: dict, routes: list) -> dict[str, str]: # that prefix as session-isolated (make_client's random-project auth # scope). Keep it. base = f"rest_mock_gen_{slug(r['via'])}" - parts.append(SUCCESS_TMPL.format( - test_name=f"{base}_ok", endpoint=endpoint, call=call, method=r["method"])) - parts.append(ERROR_TMPL.format( - test_name=f"{base}_err", endpoint=endpoint, call=call)) + parts.append( + SUCCESS_TMPL.format( + test_name=f"{base}_ok", + endpoint=endpoint, + call=call, + method=r["method"], + ) + ) + parts.append( + ERROR_TMPL.format(test_name=f"{base}_err", endpoint=endpoint, call=call) + ) fname = f"{GEN_PREFIX}{ns.replace('-', '_')}.cpp" - out[fname] = "".join(parts) + out[fname] = clang_format_source( + "".join(parts), assume_filename=f"tests/{fname}" + ) return out def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--registry-json", type=Path, default=None) - ap.add_argument("--check", action="store_true", - help="regenerate + diff vs checked-in files (GEN-FRESH-TESTS)") + ap.add_argument( + "--check", + action="store_true", + help="regenerate + diff vs checked-in files (GEN-FRESH-TESTS)", + ) args = ap.parse_args(argv) if args.registry_json: @@ -255,15 +289,23 @@ def main(argv: list[str]) -> int: if args.check: stale = [] for fname, content in files.items(): - existing = (TESTS_DIR / fname).read_text() if (TESTS_DIR / fname).exists() else None + existing = ( + (TESTS_DIR / fname).read_text() + if (TESTS_DIR / fname).exists() + else None + ) if existing != content: stale.append(fname) # also flag any checked-in generated file the generator no longer emits - for p in TESTS_DIR.glob(f"{GEN_PREFIX}*.cpp"): - if p.name not in files: - stale.append(p.name + " (orphan)") + stale.extend( + p.name + " (orphan)" + for p in TESTS_DIR.glob(f"{GEN_PREFIX}*.cpp") + if p.name not in files + ) if stale: - print("GEN-FRESH-TESTS: generated REST test files are STALE:", file=sys.stderr) + print( + "GEN-FRESH-TESTS: generated REST test files are STALE:", file=sys.stderr + ) for s in stale: print(f" {s}", file=sys.stderr) print(" run: python3 scripts/generate_rest_tests.py", file=sys.stderr) @@ -273,7 +315,9 @@ def main(argv: list[str]) -> int: for fname, content in files.items(): (TESTS_DIR / fname).write_text(content) - print(f"generate_rest_tests: wrote {len(files)} file(s), {len(plan['routes'])} routes.") + print( + f"generate_rest_tests: wrote {len(files)} file(s), {len(plan['routes'])} routes." + ) return 0 diff --git a/scripts/generate_swaig_payloads.py b/scripts/generate_swaig_payloads.py index 9c0e83b..b57db68 100644 --- a/scripts/generate_swaig_payloads.py +++ b/scripts/generate_swaig_payloads.py @@ -13,11 +13,15 @@ * ``post-prompt.yaml`` -> signalwire.core.post_prompt_generated (14 structs) one struct per components/schemas OBJECT schema; the oneOf alias ``PostPromptCallLogEntry`` is NOT surfaced (15 schemas - 1 alias = 14). - * ``swaig-response.yaml`` -> signalwire.core.swaig_actions_generated (4 structs) + * ``swaig-response.yaml`` -> signalwire.core.swaig_actions_generated (6 structs) one ``Action`` struct per action key whose value is an object-with- - properties (a bare object OR the object variant of a oneOf). + properties (a bare object OR the object variant of a oneOf), PLUS the two + envelope schemas the file itself declares — ``SwaigAction`` (the action + object) and ``SwaigResponse`` (the handler's response body). The envelopes + live in THIS module because it owns swaig-response.yaml, which is what makes + post-prompt.yaml's cross-file refs into it resolvable. - 2 + 14 + 4 = 20 structs == the surface oracle EXACTLY (0 missing / 0 extra). + 2 + 14 + 6 = 22 structs == the surface oracle EXACTLY (0 missing / 0 extra). All are METHOD-LESS DTOs (the SURFACE oracle records the bare struct name). Output: one struct per file under a per-module subdir @@ -33,6 +37,7 @@ python3 scripts/generate_swaig_payloads.py --check # GEN-FRESH: fail if stale python3 scripts/generate_swaig_payloads.py --out DIR # scratch: emit into DIR """ + from __future__ import annotations import argparse @@ -44,7 +49,9 @@ def _load_rest_generator(): here = Path(__file__).resolve().parent - spec = importlib.util.spec_from_file_location("generate_rest", here / "generate_rest.py") + spec = importlib.util.spec_from_file_location( + "generate_rest", here / "generate_rest.py" + ) if spec is None or spec.loader is None: # pragma: no cover raise SystemExit("generate_swaig_payloads.py: cannot load generate_rest.py") mod = importlib.util.module_from_spec(spec) @@ -81,7 +88,9 @@ def _load_yaml(path: Path) -> dict: def _emit(ns_segs, subdir, name, props, desc): fn = "/".join(subdir) + f"/{GR.snake(name)}.hpp" - src = GR.emit_methodless_struct(ns_segs, name, props, desc, "generate_swaig_payloads.py") + src = GR.emit_methodless_struct( + ns_segs, name, props, desc, "generate_swaig_payloads.py" + ) return fn, src @@ -97,11 +106,17 @@ def _build_swaig_request(psdk: Path) -> dict: outs: dict = {} arg = props.get("argument") if isinstance(arg, dict) and arg.get("properties"): - fn, src = _emit(SR_NS, SR_SUBDIR, "SwaigArgument", arg["properties"], - "inline swaig-request `argument` object.") + fn, src = _emit( + SR_NS, + SR_SUBDIR, + "SwaigArgument", + arg["properties"], + "inline swaig-request `argument` object.", + ) outs[fn] = src - fn, src = _emit(SR_NS, SR_SUBDIR, "SwaigRequest", props, - "swaig-request `SwaigRequest` schema.") + fn, src = _emit( + SR_NS, SR_SUBDIR, "SwaigRequest", props, "swaig-request `SwaigRequest` schema." + ) outs[fn] = src return outs @@ -118,18 +133,29 @@ def _build_post_prompt(psdk: Path) -> dict: if name in emitted: continue emitted.add(name) - fn, src = _emit(PP_NS, PP_SUBDIR, name, node.get("properties") or {}, - f"post-prompt components/schemas {raw_name!r}.") + fn, src = _emit( + PP_NS, + PP_SUBDIR, + name, + node.get("properties") or {}, + f"post-prompt components/schemas {raw_name!r}.", + ) outs[fn] = src return outs def _build_swaig_actions(psdk: Path) -> dict: spec = _load_yaml(psdk / "swaig-specs" / "swaig-response.yaml") - actions = spec["components"]["schemas"]["SwaigAction"]["properties"] + schemas = spec["components"]["schemas"] + action_schema = schemas["SwaigAction"] + actions = action_schema["properties"] def _is_obj(s) -> bool: - return isinstance(s, dict) and s.get("type") == "object" and bool(s.get("properties")) + return ( + isinstance(s, dict) + and s.get("type") == "object" + and bool(s.get("properties")) + ) outs: dict = {} emitted: set = set() @@ -143,13 +169,46 @@ def _is_obj(s) -> bool: if not _is_obj(b): continue obj_i += 1 - name = GR.type_name(_pascal_verb(verb) + "Action" + ("" if obj_i == 1 else str(obj_i))) + name = GR.type_name( + _pascal_verb(verb) + "Action" + ("" if obj_i == 1 else str(obj_i)) + ) if name in emitted: continue emitted.add(name) - fn, src = _emit(SA_NS, SA_SUBDIR, name, b.get("properties") or {}, - f"swaig-response action {verb!r} value object.") + fn, src = _emit( + SA_NS, + SA_SUBDIR, + name, + b.get("properties") or {}, + f"swaig-response action {verb!r} value object.", + ) outs[fn] = src + + # The response ENVELOPE types. The loop above lifts each action verb's inline + # object into a named Action struct, but the two schemas swaig-response.yaml + # ACTUALLY DECLARES — SwaigAction (the action object, one or more verb keys set at + # once) and SwaigResponse (the {response, action, post_process} body a handler + # returns) — were never emitted at all: the generator reached THROUGH SwaigAction + # into its .properties and dropped the envelope on the floor. They must exist here + # because this is the module that owns swaig-response.yaml, which is what makes + # post-prompt.yaml's cross-file + # ``swaig-response.yaml#/components/schemas/SwaigResponse`` refs + # (PostPromptSwaigLogEntry.post_response / .delayed_post_response) resolvable — + # the same reason the reference hosts them in this module (CROSS_FILE_MODULES in + # porting-sdk generate_python_rest_types.py). Emitted from the spec's own schemas, + # so the envelope and the per-verb structs cannot drift. + for name in ("SwaigAction", "SwaigResponse"): + node = schemas[name] + desc = (node.get("description") or "").split("\n")[0].strip() + fn, src = _emit( + SA_NS, + SA_SUBDIR, + name, + node.get("properties") or {}, + f"swaig-response components/schemas {name!r}." + + (f" {desc}" if desc else ""), + ) + outs[fn] = src return outs @@ -168,15 +227,19 @@ def build_outputs(psdk: Path) -> dict: def main(argv: list) -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--check", action="store_true", help="GEN-FRESH: exit non-zero if stale") + ap.add_argument( + "--check", action="store_true", help="GEN-FRESH: exit non-zero if stale" + ) ap.add_argument("--out", default="", help="scratch: emit into this dir") args = ap.parse_args(argv) psdk = resolve_porting_sdk() outs = build_outputs(psdk) # Only C++ headers are formatted; any .json sidecars are emitted verbatim. - outs = {fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) - for fn, src in outs.items()} + outs = { + fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) + for fn, src in outs.items() + } out_dir = Path(args.out) if args.out else repo_root() / "include" / "signalwire" @@ -196,11 +259,15 @@ def main(argv: list) -> int: if rel not in expected: stale.append(f"{p} (leftover — not in generator output)") if stale: - sys.stderr.write("GEN-FRESH FAIL: %d generated SWAIG-payload file(s) stale:\n" % len(stale)) + sys.stderr.write( + f"GEN-FRESH FAIL: {len(stale)} generated SWAIG-payload file(s) stale:\n" + ) for s in stale: - sys.stderr.write(" - %s\n" % s) + sys.stderr.write(f" - {s}\n") return 1 - print("GEN-FRESH: generated SWAIG-payload files match porting-sdk/swaig-specs/.") + print( + "GEN-FRESH: generated SWAIG-payload files match porting-sdk/swaig-specs/." + ) return 0 for fn, src in outs.items(): diff --git a/scripts/generate_swml_verbs.py b/scripts/generate_swml_verbs.py index 3361143..c4ab03a 100644 --- a/scripts/generate_swml_verbs.py +++ b/scripts/generate_swml_verbs.py @@ -31,6 +31,7 @@ python3 scripts/generate_swml_verbs.py --check # GEN-FRESH: fail if stale python3 scripts/generate_swml_verbs.py --out DIR # scratch: emit into DIR """ + from __future__ import annotations import argparse @@ -43,7 +44,9 @@ def _load_rest_generator(): here = Path(__file__).resolve().parent - spec = importlib.util.spec_from_file_location("generate_rest", here / "generate_rest.py") + spec = importlib.util.spec_from_file_location( + "generate_rest", here / "generate_rest.py" + ) if spec is None or spec.loader is None: # pragma: no cover raise SystemExit("generate_swml_verbs.py: cannot load generate_rest.py") mod = importlib.util.module_from_spec(spec) @@ -123,21 +126,30 @@ def build_outputs(psdk: Path) -> dict: outs: dict = {} emitted_names: set = set() - def emit(name: str, props: dict, desc: str, schema_name: "str | None" = None) -> None: + def emit(name: str, props: dict, desc: str, schema_name: str | None = None) -> None: if name in emitted_names: return emitted_names.add(name) fn = "/".join(SWML_VERBS_SUBDIR) + f"/{GR.snake(name)}.hpp" - outs[fn] = GR.emit_methodless_struct(SWML_VERBS_NS, name, props, desc, - "generate_swml_verbs.py", - schema_name=schema_name) + outs[fn] = GR.emit_methodless_struct( + SWML_VERBS_NS, + name, + props, + desc, + "generate_swml_verbs.py", + schema_name=schema_name, + ) # 1. One data struct per OBJECT $defs schema. for raw_name, node in defs.items(): if not isinstance(node, dict) or not GR.is_object_schema(node): continue - emit(GR.type_name(raw_name), node.get("properties") or {}, - f"schema.json $defs schema {raw_name!r}.", schema_name=raw_name) + emit( + GR.type_name(raw_name), + node.get("properties") or {}, + f"schema.json $defs schema {raw_name!r}.", + schema_name=raw_name, + ) # 2. One Config struct per flattenable SWMLMethod.anyOf verb. sm = defs.get("SWMLMethod") @@ -159,23 +171,30 @@ def emit(name: str, props: dict, desc: str, schema_name: "str | None" = None) -> props = _flatten_union(defs, inner) if not props: continue - emit(GR.type_name(_pascal(verb) + "Config"), props, - f"flattened SWMLMethod verb {verb!r} config.") + emit( + GR.type_name(_pascal(verb) + "Config"), + props, + f"flattened SWMLMethod verb {verb!r} config.", + ) return outs def main(argv: list) -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--check", action="store_true", help="GEN-FRESH: exit non-zero if stale") + ap.add_argument( + "--check", action="store_true", help="GEN-FRESH: exit non-zero if stale" + ) ap.add_argument("--out", default="", help="scratch: emit into this dir") args = ap.parse_args(argv) psdk = resolve_porting_sdk() outs = build_outputs(psdk) # Only C++ headers are formatted; any .json sidecars are emitted verbatim. - outs = {fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) - for fn, src in outs.items()} + outs = { + fn: (format_generated_cpp(src) if fn.endswith((".hpp", ".h")) else src) + for fn, src in outs.items() + } out_dir = Path(args.out) if args.out else repo_root() / "include" / "signalwire" @@ -193,11 +212,15 @@ def main(argv: list) -> int: if rel not in expected: stale.append(f"{p} (leftover — not in generator output)") if stale: - sys.stderr.write("GEN-FRESH FAIL: %d generated SWML-verb file(s) stale:\n" % len(stale)) + sys.stderr.write( + f"GEN-FRESH FAIL: {len(stale)} generated SWML-verb file(s) stale:\n" + ) for s in stale: - sys.stderr.write(" - %s\n" % s) + sys.stderr.write(f" - {s}\n") return 1 - print("GEN-FRESH: generated SWML-verb files match porting-sdk/schema.json ($defs).") + print( + "GEN-FRESH: generated SWML-verb files match porting-sdk/schema.json ($defs)." + ) return 0 for fn, src in outs.items(): diff --git a/scripts/run-ci.sh b/scripts/run-ci.sh index 8d52db4..64de16a 100755 --- a/scripts/run-ci.sh +++ b/scripts/run-ci.sh @@ -255,15 +255,15 @@ test_gate() { # "dump did not emit valid JSON". cmake --build build --target emit_corpus emit_skills \ wire_dump swml_dump strict_render_dump state_dump http_dump wire_relay_dump doc_wire_dump \ - pagination_dump relay_liveness_dump ai_chat_dump \ - secure_default_dump secret_scrub_dump -j"$(sw_build_jobs)" || return 1 + pagination_dump relay_liveness_dump wait_liveness_dump ai_chat_dump \ + secure_default_dump secret_scrub_dump token_interop_mint -j"$(sw_build_jobs)" || return 1 bash "$PORT_ROOT/scripts/run-tests.sh" ;; exec:*) local c="${BUILD_MODE#exec:}" docker exec "$c" bash -c " cmake -S '$SWCPP_CONTAINER_REPO' -B '$SWCPP_CONTAINER_BUILD' -DCMAKE_BUILD_TYPE=Release \ - && cmake --build '$SWCPP_CONTAINER_BUILD' --target run_tests emit_corpus emit_skills wire_dump swml_dump strict_render_dump state_dump http_dump wire_relay_dump doc_wire_dump pagination_dump relay_liveness_dump ai_chat_dump secure_default_dump secret_scrub_dump -j\"\$(nproc)\" \ + && cmake --build '$SWCPP_CONTAINER_BUILD' --target run_tests emit_corpus emit_skills wire_dump swml_dump strict_render_dump state_dump http_dump wire_relay_dump doc_wire_dump pagination_dump relay_liveness_dump wait_liveness_dump ai_chat_dump secure_default_dump secret_scrub_dump token_interop_mint -j\"\$(nproc)\" \ && '$SWCPP_CONTAINER_BUILD/run_tests'" ;; run:*) @@ -272,7 +272,7 @@ test_gate() { # adjacency walk) and use --network host to reach host-run mocks. docker run --rm --network host -v "$(dirname "$PORT_ROOT")":/src "$img" bash -c " cmake -S '$SWCPP_CONTAINER_REPO' -B '$SWCPP_CONTAINER_BUILD' -DCMAKE_BUILD_TYPE=Release \ - && cmake --build '$SWCPP_CONTAINER_BUILD' --target run_tests emit_corpus emit_skills wire_dump swml_dump strict_render_dump state_dump http_dump wire_relay_dump doc_wire_dump pagination_dump relay_liveness_dump ai_chat_dump secure_default_dump secret_scrub_dump -j\"\$(nproc)\" \ + && cmake --build '$SWCPP_CONTAINER_BUILD' --target run_tests emit_corpus emit_skills wire_dump swml_dump strict_render_dump state_dump http_dump wire_relay_dump doc_wire_dump pagination_dump relay_liveness_dump wait_liveness_dump ai_chat_dump secure_default_dump secret_scrub_dump token_interop_mint -j\"\$(nproc)\" \ && '$SWCPP_CONTAINER_BUILD/run_tests'" ;; *) @@ -398,8 +398,11 @@ _fresh_surface_cache_path() { echo "$PORT_ROOT/.sw-tmp/fresh_port_surface.json"; # place so you never hand-run it; notes if it changed files, then re-checks. # * CI ($CI=true) -> `clang-format --dry-run -Werror` (read-only): FAILS # if any unformatted source reached CI. -# Scope is first-party src/ + include/ ONLY — vendored deps/ (httplib.h, -# json.hpp, nlohmann/) and the FetchContent IXWebSocket tree are never touched. +# Scope is EVERY first-party C++ tree — src/ include/ tools/ tests/ examples/ +# rest/examples/ relay/examples/ (widened 2026-07-30; this comment previously +# said "src/ + include/ ONLY" and was already stale, since tools/ was in scope). +# Only genuinely third-party code is excluded: vendored deps/ (httplib.h, +# json.hpp, nlohmann/) and the FetchContent IXWebSocket tree. # clang-format runs on the host regardless of BUILD_MODE (no compiler/SDK # needed — it only parses tokens). # The FMT gate now delegates to the CANONICAL scripts/run-format.sh (single @@ -437,6 +440,15 @@ lint_gate() { bash "$PORT_ROOT/scripts/run-lint.sh" } +# PY-LINT gate: ruff (lint + format) over the hand-written Python under +# scripts/. Delegates to the CANONICAL scripts/run-pylint.sh, and follows the +# same LOCAL-applies / CI-checks contract as fmt_gate above: an unformatted +# commit must not be green locally and red in CI on the very formatting the +# local run applied. +pylint_gate() { + bash "$PORT_ROOT/scripts/run-pylint.sh" ${CI:+--check} +} + # STRICT-MOCKS (§2.2 / Part 1.4): re-run the RELAY mock suite with mock_relay in # STRICT mode (MOCK_RELAY_STRICT=1 → 400s an unknown field / duplicate id instead # of tolerantly journaling it) so a wire-shape regression fails loud. cpp's relay @@ -540,8 +552,22 @@ run_gate "SURFACE" "surface parity suite (SIGNATURES/DRIFT/SURFACE-FRESH/SURFACE # red. RATCHET, not a hard gate: dynamic languages cannot always express a type, so this # banks the current count and fails only on REGRESSION. Drive the number DOWN; never up. # Runs after SURFACE because it reads the port_signatures.json that enumeration writes. -run_gate "TYPE-EROSION" "port did not erase a reference-declared param type (ratchet 122)" \ - python3 "$PORTING_SDK_DIR/scripts/diff_port_type_erosion.py" --port cpp --repo "$PORT_ROOT" --max 122 +run_gate "TYPE-EROSION" "port did not erase a reference-declared param type (ratchet 85)" \ + python3 "$PORTING_SDK_DIR/scripts/diff_port_type_erosion.py" --port cpp --repo "$PORT_ROOT" --max 85 + +# SIGNATURES-FRESH: the committed port_signatures.json must match a fresh regen. +# Nothing previously guarded it — SURFACE-FRESH covers only port_surface.json. +# That artifact is DRIFT's INPUT, so a stale one means the parity gate compares +# against a fiction and reports clean while real drift hides behind it. +# +# STANDALONE, deliberately not a _surface_commands.py table entry: only 8 of the +# 10 run-ci scripts read that table, so a table entry would be silently skipped +# in the two that do not. cpp schedules serially via run_gate rather than the DAG +# scheduler, so this is the run_gate form of the other ports' sched_gate line. +# Placed after SURFACE because it regenerates the same artifacts that gate reads. +run_gate "SIGNATURES-FRESH" "committed port_signatures.json matches a fresh regen" \ + python3 "$PORTING_SDK_DIR/scripts/suites/_signatures_fresh.py" \ + --port cpp --repo "$PORT_ROOT" --porting-sdk "$PORTING_SDK_DIR" # GEN (regen-from-specs family): GEN-FRESH/-SWML/-RELAY/-SWAIG/-TESTS. # GEN-FRESH-TESTS reuses cpp's route_registry binary via the suite's cpp branch. @@ -557,9 +583,9 @@ run_gate "GEN" "generated-code freshness suite (GEN-FRESH/-SWML/-RELAY/-SWAIG/-T # build/secure_default_dump and proves define_tool defaults secure=TRUE and that # the wire reflects it (the per-tool __token on the rendered webhook). Without it # a regression that silently ships every tool unauthenticated goes undetected. -run_gate "BEHAVIORAL" "behavioral suite, per-PR rules (BEHAVIORAL-*/EMISSION/SKILL-CONTRACT/SWAIG-*/ERROR-ENVELOPE/PAGINATION-WIRED/DOC-WIRE/REST-COVERAGE/SPEC-PARITY/SECURE-DEFAULT)" \ +run_gate "BEHAVIORAL" "behavioral suite, per-PR rules (BEHAVIORAL-*/EMISSION/SKILL-CONTRACT/SWAIG-*/ERROR-ENVELOPE/PAGINATION-WIRED/DOC-WIRE/REST-COVERAGE/SPEC-PARITY/SECURE-DEFAULT/BEHAVIORAL-STRICT-RENDER/CA-VAR/TLS-VERIFY/SECRET-SCRUB)" \ python3 "$PORTING_SDK_DIR/scripts/suites/behavioral.py" --port cpp --repo "$PORT_ROOT" \ - --rules REST-COVERAGE,SPEC-PARITY,EMISSION,BEHAVIORAL-WIRE,BEHAVIORAL-SWML,BEHAVIORAL-STATE,BEHAVIORAL-HTTP,BEHAVIORAL-WIRE-RELAY,SKILL-CONTRACT,SWAIG-COVERAGE,SWAIG-CLI,ERROR-ENVELOPE,PAGINATION-WIRED,PAGINATION-CORPUS,DOC-WIRE,SECURE-DEFAULT + --rules REST-COVERAGE,SPEC-PARITY,EMISSION,BEHAVIORAL-WIRE,BEHAVIORAL-SWML,BEHAVIORAL-STATE,BEHAVIORAL-HTTP,BEHAVIORAL-WIRE-RELAY,SKILL-CONTRACT,SWAIG-COVERAGE,SWAIG-CLI,ERROR-ENVELOPE,PAGINATION-WIRED,PAGINATION-CORPUS,DOC-WIRE,SECURE-DEFAULT,BEHAVIORAL-STRICT-RENDER,CA-VAR,TLS-VERIFY,SECRET-SCRUB # BEHAVIORAL-NIGHTLY: the timing-sensitive connection-liveness dumps. # WAIT-LIVENESS (Action::wait() blocks-until-event) + RELAY-LIVENESS (the broader @@ -576,6 +602,22 @@ run_gate "BEHAVIORAL-NIGHTLY" "behavioral suite, nightly rules (WAIT-LIVENESS/RE python3 "$PORTING_SDK_DIR/scripts/suites/behavioral.py" --port cpp --repo "$PORT_ROOT" \ --rules WAIT-LIVENESS,RELAY-LIVENESS,SECRET-SCRUB-LIVE +# TOKEN-INTEROP — property 3 of the SWAIG tool-token contract: a token this port MINTS +# must validate under the REFERENCE's own decoder. SECURE-DEFAULT proves a token is +# minted and the fleet keying check proves the HMAC key; NEITHER sees the base64 +# ENVELOPE, so a port can ship correct-key correct-HMAC tokens that no other +# implementation accepts — in production every secure tool call then fails auth. This +# port shipped exactly that: base64url_encode popped the '=' padding (while its own +# header comment claimed it matched the reference's urlsafe_b64encode), and the +# reference's urlsafe_b64decode RAISES on a stripped '='. Our base64url_decode tolerates +# missing padding, so round-tripping against ourselves could never catch it. The mint +# binary (build/token_interop_mint) is built with the other dump binaries above. One +# mint + a pure-python validation → cheap, per-PR (a security property must not wait +# for nightly). +run_gate "TOKEN-INTEROP" "a token this port mints validates under the reference's decoder (padded urlsafe base64, ':'-signed / '.'-enveloped, hex HMAC keyed by the secret_key string)" \ + python3 "$PORTING_SDK_DIR/scripts/diff_port_token_interop.py" --port cpp \ + --mint-cmd "$PORT_ROOT/build/token_interop_mint" + # DOC-TRUTH (one markdown walk): DOC-AUDIT/DOC-LINKS/DOC-LANG-PURITY/DOC-ENV/ # COUNT-CLAIM/ACCESSOR-TRUTH/STATUS-CLAIM/README-INCLUDE. run_gate "DOC-TRUTH" "doc-truth suite (DOC-AUDIT/DOC-LINKS/DOC-LANG-PURITY/DOC-ENV/COUNT-CLAIM/ACCESSOR-TRUTH/STATUS-CLAIM/README-INCLUDE)" \ @@ -610,6 +652,15 @@ run_gate "FMT" "clang-format (.clang-format; local: apply, CI: check)" fmt_gate # LINT — clang-tidy curated set burned to zero (WarningsAsErrors:'*') run_gate "LINT" "clang-tidy curated set, zero findings" lint_gate +# PY-LINT — ruff over the 9 hand-written Python files under scripts/ (~10.4k +# lines), which no gate covered before 2026-07-30 even though two of them +# (_cpp_fmt.py, clang_tidy_cache.py) are the lint/format infrastructure the FMT +# and LINT gates above run THROUGH. Rule selection mirrors the reference +# implementation's (signalwire-python/pyproject.toml); config in ruff.toml. +# Dual-mode exactly like FMT: LOCAL applies fixes in place, CI ($CI set) passes +# --check for the read-only verification. +run_gate "PY-LINT" "ruff over scripts/*.py (local: apply, CI: check)" pylint_gate + # DEAD-PUBLIC-ERROR — exported error types are raised/caught/user-signalled run_gate "DEAD-PUBLIC-ERROR" "exported error types are raised/caught/user-signalled (no dead error surface)" \ python3 "$PORTING_SDK_DIR/scripts/dead_public_error.py" --port cpp --repo . @@ -680,19 +731,15 @@ run_gate "WIRED-MODES" "load-bearing run-ci modes present (WIRED_MODES.md merge- wired_modes_gate # DOC-SURFACE (plan §6.3): doxygen-header coverage floor on the public surface. The -# floor is pinned in .doc_surface_floor (90.2% today) and ratchets up via -# --write-floor; report-only at graduation, so a doc regression is visible without -# failing the run yet (never-regress is enforced once the floor flips blocking). -# GUARDED like WIRED-MODES: doc_surface.py is a porting-sdk plan-branch dep. -doc_surface_gate() { - if [ -f "$PORTING_SDK_DIR/scripts/doc_surface.py" ]; then - python3 "$PORTING_SDK_DIR/scripts/doc_surface.py" --port cpp --repo "$PORT_ROOT" --report-only - else - echo "[doc-surface] doc_surface.py not on porting-sdk main yet — skip-pass (plan-branch dep)" - fi -} -run_gate "DOC-SURFACE" "public doc-comment coverage floor (.doc_surface_floor ratchet; report-only)" \ - doc_surface_gate +# floor is pinned in .doc_surface_floor (100.0% as of the 2026-07-29 burn) and ratchets +# up via --write-floor. BLOCKING: every public class carries a doxygen header, so any +# new undocumented one is a real regression and must red the run, not merely be noted. +# The skip-with-pass guard is GONE. It existed for when doc_surface.py lived only on a +# porting-sdk plan branch; the script is on the pinned PORTING_SDK_REF now, so the branch is +# dead — and a MISSING gate script must FAIL, not pass. Guarding it made "BLOCKING" above a +# lie: a path typo or a bad checkout would have disabled the gate under a green tick. +run_gate "DOC-SURFACE" "public doc-comment coverage floor (.doc_surface_floor ratchet; 100% — blocking)" \ + python3 "$PORTING_SDK_DIR/scripts/doc_surface.py" --port cpp --repo "$PORT_ROOT" # GATE-INVENTORY NOTE (plan §2.16): porting-sdk/GATE_INVENTORY.md is generated by # gen_gate_inventory.py from the REFERENCE port's run-ci.sh (typescript — the diff --git a/scripts/run-format.sh b/scripts/run-format.sh index 946524d..2289008 100755 --- a/scripts/run-format.sh +++ b/scripts/run-format.sh @@ -13,19 +13,31 @@ # non-zero if anything is unformatted. This is the # dual-mode CI FMT gate. # -# Scope is the first-party src/ + include/ + tools/ trees ONLY — both the -# hand-written and the GENERATED headers (which are already clang-format-clean by -# construction, so --check stays green — AGENT_RULES §5). Vendored deps/ -# (httplib.h, json.hpp, nlohmann/) and the FetchContent IXWebSocket tree are -# never touched. +# Scope is EVERY first-party C++ tree — src/ include/ tools/ tests/ examples/ +# rest/examples/ relay/examples/ — both the hand-written and the GENERATED +# headers (which are already clang-format-clean by construction, so --check stays +# green — AGENT_RULES §5). +# +# Widened 2026-07-30 from src/+include/+tools/ to the whole first-party tree. +# tests/ and examples/ were previously outside the fmt scope with no stated +# rationale — the only exclusion this repo ever justified is vendored code. +# Per the owner: "all the full directories should be linted and formatted +# including tests examples and all ... examples and tests are shipping code too." +# There is ONE bar and it is the bar the shipped library already meets. +# +# The ONLY thing still excluded is genuinely third-party code we do not own: +# vendored deps/ (httplib.h, json.hpp, nlohmann/) and the FetchContent +# IXWebSocket tree. Do not add a directory exclusion here for first-party code. source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh" cd "$REPO" -# Same source set as run-ci's fmt_gate: first-party C++ under src/ include/ tools/. +# Same source set as run-ci's fmt_gate: EVERY first-party C++ tree. rest/ and +# relay/ are listed by their examples/ subdir because the rest of those trees is +# markdown docs, not source. fmt_sources() { - find src include tools -type f \ + find src include tools tests examples rest/examples relay/examples -type f \ \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' -o -name '*.cc' \) \ 2>/dev/null } diff --git a/scripts/run-lint.sh b/scripts/run-lint.sh index 1e0e429..fe23b0b 100755 --- a/scripts/run-lint.sh +++ b/scripts/run-lint.sh @@ -95,12 +95,104 @@ else tidy_cmd=("$ct") fi -# bash-3.2 compatible (macOS default): stream find -> xargs, NUL-delimited so paths -# with spaces are safe. -n 1 = one clang-tidy (cached) per file; -P = fan across cores. -if ! find src include -name '*.cpp' | grep -q .; then +# SCOPE (widened 2026-07-30). Previously src/ + include/ only. tools/ (19 TUs) +# was FORMATTED but never LINTED, and examples/ (73 TUs across examples/, +# rest/examples/, relay/examples/) was neither -- all excluded by this hard +# `find` path list with no rationale anywhere. The only exclusion this repo ever +# WROTE DOWN a reason for is vendored deps/, and that reason ("third-party code +# we do not own") does not extend to our own tools or our own shipped examples. +# +# Per the owner: examples and tests are shipping code too, and there is ONE bar. +# Linting these trees found real defects -- 11 discarded [[nodiscard]] results in +# the RELAY/REST examples, 7 atoi/atof calls that turned a bad PORT into port 0, +# and 87 main() functions that answered an exception with std::terminate. +# +# deps/ stays out, now enforced at the compiler (CMake marks it a SYSTEM include) +# rather than by a path list, because clang-diagnostic-* are compiler warnings +# that --header-filter cannot reach. +# +# tests/ is in scope too, via its own invocation below (three checks scoped off, +# owner-ruled). The header-filter must therefore cover tests/ as well, so that +# findings inside the 123 .cpp files #included into test_main.cpp are reported at +# their OWN file:line rather than being filtered out as "not the main file". +HEADER_FILTER='signalwire-cpp/(src|include|tools|tests|examples|rest|relay)/' + +if ! find src include tools examples rest/examples relay/examples -name '*.cpp' | grep -q .; then echo "no C++ sources found to lint" >&2; exit 1 fi -find src include -name '*.cpp' -print0 \ +find src include tools examples rest/examples relay/examples -name '*.cpp' -print0 \ | xargs -0 -P "$jobs" -n 1 "${tidy_cmd[@]}" -p "$tidy_build" \ - --header-filter='signalwire-cpp/(src|include)/' --quiet + --header-filter="$HEADER_FILTER" --quiet +shipped_rc=$? + +# --------------------------------------------------------------------------- +# tests/ — SAME .clang-tidy, SAME WarningsAsErrors, burned to ZERO, with exactly +# THREE checks scoped off (owner-ruled 2026-07-30). This is a separate +# invocation only because those three exclusions are scoped; it is NOT a second +# config and NOT a looser tier. Every other check is ON for tests/, and tests/ +# is fully covered by the FMT gate. +# +# The three, each with the reason it is wrong FOR THIS CONTEXT: +# +# performance-unnecessary-copy-initialization +# MACRO ARTIFACT + a real correctness hazard. 319 of its 320 findings come +# from `auto _a = (a)` inside ASSERT_EQ/ASSERT_NE, not from test source, and +# that copy is LOAD-BEARING: binding by const& instead makes +# `mock.requests()[0].method` a reference into a by-value temporary that +# dies at the end of the full expression. Measured, not assumed — the change +# fails 5 tests. A rule whose remedy introduces dangling references into +# correct code is the rule being wrong here. +# +# readability-simplify-boolean-expr +# MACRO ARTIFACT, 231 of 231. ASSERT_TRUE(x) expands to `if (!(x))`, and the +# check then proposes applying DeMorgan to the MACRO's negation of the +# caller's compound condition. There is nothing in test source to simplify. +# +# bugprone-suspicious-include +# THE DOCUMENTED ARCHITECTURE, 123 of 123. test_main.cpp #includes 123 .cpp +# files on purpose so the suite is one translation unit — CLAUDE.md:96: +# "All test files are #included into test_main.cpp and compiled as one +# translation unit." +# +# Everything else in tests/ was BURNED, not excused: discarded [[nodiscard]] +# results, unchecked ::bind/getsockname, swallowed MOCK_*_PORT parse errors, +# exceptions escaping std::thread bodies, std::system("mkdir -p"), and 21 +# unchecked optional dereferences. +tests_checks='-performance-unnecessary-copy-initialization' +tests_checks="$tests_checks,-readability-simplify-boolean-expr" +tests_checks="$tests_checks,-bugprone-suspicious-include" + +# Analyse only the files that are REAL translation units. The other 123 test +# .cpp files are #included into test_main.cpp and compiled as one TU, so they +# have no compile_commands entry -- handing them to clang-tidy directly would +# ERROR ("Compile command not found"), not analyse. They ARE analysed, through +# test_main.cpp, and $HEADER_FILTER covers tests/ so each finding is reported at +# its own file:line. Proven: planting a violation in an #included test file makes +# this gate report it against THAT file. +tests_tus="tests/test_main.cpp tests/mocktest.cpp tests/relay_mocktest.cpp tests/tls_mocktest.cpp" + +# Guard the list against going stale: any tests/*.cpp that is NOT one of the four +# and is NOT #included by test_main.cpp would be silently unanalysed. Fail loud +# instead of quietly shrinking the gate's coverage. +missing="" +for f in tests/*.cpp; do + case " $tests_tus " in *" $f "*) continue ;; esac + base="${f##*/}" + grep -q "#include \"$base\"" tests/test_main.cpp || missing="$missing $f" +done +if [ -n "$missing" ]; then + echo "run-lint: these tests/*.cpp are neither a listed TU nor #included by" >&2 + echo " test_main.cpp, so nothing would analyse them:$missing" >&2 + echo " Add them to \$tests_tus (with a compile_commands entry) or to" >&2 + echo " test_main.cpp's include list." >&2 + exit 1 +fi + +# shellcheck disable=SC2086 +echo $tests_tus | tr ' ' '\n' \ + | xargs -P "$jobs" -n 1 "${tidy_cmd[@]}" -p "$tidy_build" \ + --header-filter="$HEADER_FILTER" --checks="$tests_checks" --quiet +tests_rc=$? + +[ "$shipped_rc" -eq 0 ] && [ "$tests_rc" -eq 0 ] exit $? diff --git a/scripts/run-pylint.sh b/scripts/run-pylint.sh new file mode 100755 index 0000000..f6190f9 --- /dev/null +++ b/scripts/run-pylint.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# run-pylint.sh — CANONICAL Python linter/formatter for signalwire-cpp (ruff). +# +# This is a C++ SDK, but it carries 9 hand-written Python files under scripts/ +# (~10.4k lines), and until 2026-07-30 no gate linted or formatted ANY of them. +# Two are load-bearing lint/format infrastructure themselves — _cpp_fmt.py (the +# REST/type/verb generators shell to it to format their emitted C++) and +# clang_tidy_cache.py (the wrapper the LINT gate routes every clang-tidy call +# through) — so the tooling that enforced the bar was itself below any bar. +# +# Modes (mirroring scripts/run-format.sh so the two behave identically): +# (default) APPLY — `ruff check --fix` + `ruff format`: fix in place. +# --check VERIFY — `ruff check` + `ruff format --check`: read-only, exits +# non-zero on any finding. This is the CI mode. +# +# Config lives in ruff.toml at the repo root and MIRRORS the reference +# implementation's rule selection (signalwire-python/pyproject.toml) so the +# fleet stays consistent. The single excluded file, clang_tidy_cache.py, is +# VENDORED third-party code (matus-chochlik/ctcache at a pinned SHA) — the same +# category as deps/ on the C++ side, and excluded for the same reason. + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh" + +cd "$REPO" + +if ! command -v ruff >/dev/null 2>&1; then + echo "ERROR: ruff not found on PATH." >&2 + echo " It lints + formats the Python under scripts/." >&2 + echo " Install it with: pip install ruff==$SW_RUFF_VERSION" >&2 + exit 1 +fi + +# ASSERT THE PINNED VERSION, exactly as _env.sh asserts clang-format major 18 and +# for the same reason: CI installs `ruff==$SW_RUFF_VERSION` while a local dev runs +# whatever they installed months ago, so an unasserted version lets local and CI +# disagree about what passes PY-LINT — green here, red there, with no code change. +# SW_ALLOW_TOOL_VERSION_DRIFT=1 downgrades this to a warning, for a deliberate +# bump-and-reformat run only (then update _env.sh + the workflows together). +_RUFF_VERSION="$(ruff --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" +if [ "$_RUFF_VERSION" != "$SW_RUFF_VERSION" ]; then + if [ "${SW_ALLOW_TOOL_VERSION_DRIFT:-0}" = "1" ]; then + echo "WARNING: ruff is '${_RUFF_VERSION:-unknown}', not the pinned $SW_RUFF_VERSION (drift allowed)." >&2 + else + echo "ERROR: ruff on PATH is version '${_RUFF_VERSION:-unknown}', not the pinned $SW_RUFF_VERSION." >&2 + echo " CI installs exactly $SW_RUFF_VERSION, so a different version here means" >&2 + echo " local and CI disagree about what passes PY-LINT." >&2 + echo " Install the pin: pip install ruff==$SW_RUFF_VERSION" >&2 + echo " Or set SW_ALLOW_TOOL_VERSION_DRIFT=1 for a deliberate bump run." >&2 + exit 1 + fi +fi + +# Fail loud rather than silently passing on an empty file set — a gate that +# checks nothing is worse than no gate. +if ! find scripts -name '*.py' | grep -q .; then + echo "no Python sources found to lint" >&2; exit 1 +fi + +# PIN THE CONFIG EXPLICITLY. ruff resolves configuration by walking UP from the +# TARGET, not from the CWD and not from the repo root, and when no config is +# found on that walk it falls back to BUILT-IN DEFAULTS — a different ruleset, +# reported as success. That is the vacuity trap this campaign keeps paying for: +# a lint gate that silently checks something other than what you configured. +# Two sibling ports measured real drift from exactly this (one found 0 where the +# real config found 4; another had 7 findings silently change status). +CONFIG="$REPO/ruff.toml" +if [ ! -f "$CONFIG" ]; then + echo "ERROR: $CONFIG not found." >&2 + echo " Refusing to run: without it ruff would silently fall back to its" >&2 + echo " BUILT-IN DEFAULTS and report success against the wrong ruleset." >&2 + exit 1 +fi + +# Always pass the DIRECTORY, never individual files: ruff's `exclude` governs +# directory traversal, and a path named explicitly on the command line is +# analysed even when excluded. The vendored scripts/clang_tidy_cache.py relies +# on that exclusion, so handing ruff the tree (not the file) is what keeps the +# vendored file out. Verified: `ruff check --config /scripts/` and the +# relative form both report 0, from the repo root and from a foreign CWD. +if [ "${1:-}" = "--check" ]; then + ruff check --config "$CONFIG" scripts/ || exit 1 + ruff format --config "$CONFIG" --check scripts/ || exit 1 +else + ruff check --config "$CONFIG" --fix scripts/ || exit 1 + ruff format --config "$CONFIG" scripts/ || exit 1 + # A residual finding --fix cannot resolve must still fail the gate. + ruff check --config "$CONFIG" scripts/ || exit 1 + ruff format --config "$CONFIG" --check scripts/ || exit 1 +fi diff --git a/src/agent/agent_base.cpp b/src/agent/agent_base.cpp index 7567c9a..5d2f9fe 100644 --- a/src/agent/agent_base.cpp +++ b/src/agent/agent_base.cpp @@ -30,10 +30,9 @@ namespace agent { namespace { -/// Load the ``service`` section of the agent's config file, mirroring the -/// reference's ``AgentBase._load_service_config(config_file, name)``: use the -/// explicit path when given, else discover one for this service name; return -/// an empty object when nothing loads. +/// Load the ``service`` section of the agent's config file: use the explicit +/// path when given, else discover one for this service name; return an empty +/// object when nothing loads. json load_service_config(const std::optional& config_file, const std::string& service_name) { std::optional path = config_file; @@ -215,7 +214,10 @@ AgentBase::AgentBase(const AgentBase& other) global_data_ = other.global_data_; native_functions_ = other.native_functions_; internal_fillers_ = other.internal_fillers_; - debug_events_ = other.debug_events_; + // Reference copies both fields onto the ephemeral agent + // (agent_base.py:1587-1588). + debug_events_enabled_ = other.debug_events_enabled_; + debug_events_level_ = other.debug_events_level_; prompt_llm_params_ = other.prompt_llm_params_; post_prompt_llm_params_ = other.post_prompt_llm_params_; pre_answer_verbs_ = other.pre_answer_verbs_; @@ -287,9 +289,9 @@ AgentBase& AgentBase::prompt_add_section(const std::string& title, const std::st return *this; } -AgentBase& AgentBase::prompt_add_subsection(const std::string& parent_title, - const std::string& title, const std::string& body, - const std::vector& bullets) { +AgentBase& AgentBase::prompt_add_subsection( + const std::string& parent_title, const std::string& title, const std::string& body, + const std::optional>& bullets) { // #182: auto-create the parent section if it does not exist yet, matching // the TS reference (`addSubsection` calls `addSection(parentTitle)` when // missing) — previously this was a no-op for an unknown parent. @@ -301,7 +303,8 @@ AgentBase& AgentBase::prompt_add_subsection(const std::string& parent_title, PomSection sub; sub.title = title; sub.body = body; - sub.bullets = bullets; + // Reference: ``bullets or []`` — absent becomes the empty list. + sub.bullets = bullets.value_or(std::vector{}); section.subsections.push_back(std::move(sub)); break; } @@ -529,6 +532,33 @@ bool AgentBase::validate_tool_token(const std::string& function_name, const std: } } +std::optional AgentBase::swaig_validate_token( + const std::string& function_name, const std::optional& token, + const std::optional& call_id) const { + auto tool_it = tools_.find(function_name); + if (tool_it == tools_.end() || !tool_it->second.secure) { + // Unknown function (the caller reports that separately) or an explicitly + // insecure tool: never refused here, it runs ungated. + return std::nullopt; + } + + // A token can only be validated against a call_id; without one there is + // nothing to check it against, so treat it as unvalidated rather than as a + // bypass. An empty-string token counts as ABSENT, not as "present but wrong". + const bool have_token = token.has_value() && !token->empty(); + const bool valid = + have_token && call_id.has_value() && validate_tool_token(function_name, *token, *call_id); + if (valid) { + return std::nullopt; + } + + get_logger().warn("swaig secure_function_refused function=" + function_name + + " token_present=" + (have_token ? "true" : "false")); + return swaig::FunctionResult( + "I'm sorry, the security token for this function is invalid or expired. " + "I cannot execute this action."); +} + // ============================================================================ // AI Config Methods // ============================================================================ @@ -753,8 +783,9 @@ AgentBase& AgentBase::add_internal_filler(const std::string& function_name, return *this; } -AgentBase& AgentBase::enable_debug_events(bool enable) { - debug_events_ = enable; +AgentBase& AgentBase::enable_debug_events(int level) { + debug_events_enabled_ = true; + debug_events_level_ = level; return *this; } @@ -1499,14 +1530,42 @@ json AgentBase::build_swaig_functions(const std::string& webhook_url, // ``__token=`` query parameter IS the wire manifestation of ``secure`` — // the platform presents it on the callback and the /swaig dispatcher // validates it. An INSECURE tool gets no token. - std::string url = webhook_url; + std::string token; if (it->second.secure && !call_id.empty()) { - std::string token = session_manager_.create_tool_token(name, call_id); + token = session_manager_.create_tool_token(name, call_id); + } + + // WHETHER this entry gets its own ``web_hook_url`` at all — the reference + // guard at agent_base.py:1085-1099, verbatim: + // + // if func.webhook_url: -> use the external URL + // elif token or _swaig_query_params: + // -> build the local URL (+ __token) + // # else: NO web_hook_url key on the entry AT ALL + // + // The else branch is load-bearing SECURITY, not a cosmetic difference: an + // insecure tool that is handed the local URL publishes an + // UNAUTHENTICATED, function-specific callback on the wire. With no key it + // falls back to the shared ``SWAIG.defaults.web_hook_url``, which is the + // whole point of ``secure=false``. Emitting an empty string / null / a + // tokenless URL are all the same defect — the KEY must be absent. + // + // C++ has no per-tool external webhook (``ToolDefinition`` carries no + // ``webhook_url``; the agent-level override lives in ``webhook_url_`` and + // is already folded into ``webhook_url`` by build_webhook_url), so the + // first reference branch has no analog here and the guard reduces to the + // ``elif``. + const bool wants_own_webhook = !token.empty() || !swaig_query_params_.empty(); + + std::string url; + if (wants_own_webhook) { + url = webhook_url; if (!token.empty()) { url += (url.find('?') == std::string::npos ? "?" : "&"); url += "__token=" + signalwire::url_encode(token); } } + // to_swaig_json omits the key entirely for an empty url. functions.push_back(it->second.to_swaig_json(url)); } } @@ -1551,9 +1610,22 @@ json AgentBase::build_ai_verb(const std::string& webhook_url, const std::string& ai["post_prompt_url"] = pp_url; } - // AI params - if (!ai_params_.is_null() && !ai_params_.empty()) { - ai["params"] = ai_params_; + // AI params. The debug-event webhook is auto-wired INTO params when enabled, + // exactly as the reference does (agent_base.py:1248-1261): it sets + // ``params.debug_webhook_url`` and ``params.debug_webhook_level``. Neither + // the reference nor swml/schema.json has any ``ai.debug_events`` key. + json params = ai_params_.is_null() ? json::object() : ai_params_; + if (debug_events_enabled_) { + std::string debug_url = webhook_url; + auto swaig_pos = debug_url.rfind("/swaig"); + if (swaig_pos != std::string::npos) { + debug_url = debug_url.substr(0, swaig_pos) + "/debug_events"; + } + params["debug_webhook_url"] = debug_url; + params["debug_webhook_level"] = debug_events_level_; + } + if (!params.empty()) { + ai["params"] = params; } // Hints @@ -1596,6 +1668,25 @@ json AgentBase::build_ai_verb(const std::string& webhook_url, const std::string& json functions = build_swaig_functions(webhook_url, call_id); if (!functions.empty()) { swaig_section["functions"] = functions; + // The SHARED fallback endpoint, emitted whenever there are functions at all + // (reference agent_base.py:1108-1113: ``if functions: ... if "defaults" not + // in swaig_obj: swaig_obj["defaults"] = {"web_hook_url": ...}``). + // + // This is the OTHER half of the build_swaig_functions webhook guard and must + // not be separated from it. That guard correctly withholds a per-tool + // ``web_hook_url`` from an INSECURE tool — but an insecure tool is not + // meant to be unreachable, it is meant to fall back to THIS shared endpoint. + // Without the defaults block the insecure tool renders with no callback + // endpoint at all, which is a worse failure than the unauthenticated + // per-tool callback the guard removed. The SECURE-DEFAULT gate inspects only + // ``functions[]``, so it cannot see this — it is pinned by test + // tool_secure_and_insecure_tools_render_divergent_webhooks instead. + // + // An explicit ``default_webhook_url`` set above wins (the reference's + // ``if "defaults" not in swaig_obj`` guard). + if (!swaig_section.contains("defaults")) { + swaig_section["defaults"] = json::object({{"web_hook_url", webhook_url}}); + } } if (!function_includes_.empty()) { swaig_section["includes"] = function_includes_; @@ -1608,6 +1699,19 @@ json AgentBase::build_ai_verb(const std::string& webhook_url, const std::string& swaig_section["mcp_servers"] = mcp_servers_; } + // Internal fillers live INSIDE the SWAIG object under their canonical name + // ``internal_fillers`` — schema ``$defs/SWAIG`` declares exactly + // [defaults, functions, includes, internal_fillers, native_functions]. + // This previously emitted ``ai.fillers``: wrong KEY and wrong NESTING LEVEL at + // once. ``$defs/AIObject`` is closed over nine keys via + // ``unevaluatedProperties: {"not": {}}`` and ``fillers`` is not one of them, so + // every document from an agent with internal fillers was schema-invalid and the + // server never read them. Reference: ``agent_base.py:1029`` + // ``swaig_obj["internal_fillers"] = ...``. + if (!internal_fillers_.is_null() && !internal_fillers_.empty()) { + swaig_section["internal_fillers"] = internal_fillers_; + } + if (!swaig_section.empty()) { ai["SWAIG"] = swaig_section; } @@ -1617,19 +1721,17 @@ json AgentBase::build_ai_verb(const std::string& webhook_url, const std::string& ai["global_data"] = global_data_; } - // Contexts + // Contexts belong INSIDE the prompt ($defs/AIPromptText / $defs/AIPromptPom), + // not at the ai top level — ``$defs/AIObject`` is closed over nine keys and + // ``contexts`` is not among them, so an agent using the steps feature emitted a + // document the schema rejects. Reference: ``swml_handler.py:191`` + // ``prompt_config["contexts"] = contexts``. (Same defect independently found in + // the typescript port.) if (context_builder_ && context_builder_->has_contexts()) { - ai["contexts"] = context_builder_->to_json(); - } - - // Debug events - if (debug_events_) { - ai["debug_events"] = true; - } - - // Internal fillers - if (!internal_fillers_.is_null() && !internal_fillers_.empty()) { - ai["fillers"] = internal_fillers_; + if (!ai.contains("prompt") || !ai["prompt"].is_object()) { + ai["prompt"] = json::object(); + } + ai["prompt"]["contexts"] = context_builder_->to_json(); } return ai; @@ -1823,9 +1925,22 @@ json AgentBase::render_swml_internal(const std::map& h swml::Document doc; + // Every verb below is schema-checked before it lands in the document (task + // #194). The document assembled here is LOCAL — it is not the Service's own + // ``document_`` — so ``Service::add_verb`` cannot be used to append to it, and + // before this fix nothing validated any of it: an agent rendered whatever the + // caller had configured, straight onto the wire. AgentBase derives from + // swml::Service, so it validates through the same protected check every + // Service-level entry point uses. + const auto emit = [this, &doc](const std::string& verb_name, const json& params) { + validate_verb_or_throw(verb_name, params); + doc.main().add_verb(verb_name, params); + }; + const auto emit_verb = [&emit](const swml::Verb& v) { emit(v.name, v.params); }; + // Phase 1: Pre-answer verbs for (const auto& v : pre_answer_verbs_) { - doc.main().add_verb(v); + emit_verb(v); } // Phase 2: Answer verb — only when auto_answer is enabled (reference: @@ -1833,30 +1948,29 @@ json AgentBase::render_swml_internal(const std::map& h // configured answer verbs still win over the default config. if (auto_answer_) { if (answer_verbs_.empty()) { - doc.main().add_verb("answer", json::object({{"max_duration", 3600}})); + emit("answer", json::object({{"max_duration", 3600}})); } else { for (const auto& v : answer_verbs_) { - doc.main().add_verb(v); + emit_verb(v); } } } // Phase 3: Post-answer verbs — recording first, as in the reference. if (record_call_) { - doc.main().add_verb("record_call", - json::object({{"format", record_format_}, {"stereo", record_stereo_}})); + emit("record_call", json::object({{"format", record_format_}, {"stereo", record_stereo_}})); } for (const auto& v : post_answer_verbs_) { - doc.main().add_verb(v); + emit_verb(v); } // Phase 4: AI verb json ai_verb = build_ai_verb(webhook_url, call_id); - doc.main().add_verb("ai", ai_verb); + emit("ai", ai_verb); // Phase 5: Post-AI verbs for (const auto& v : post_ai_verbs_) { - doc.main().add_verb(v); + emit_verb(v); } json swml = doc.to_json(); @@ -2009,24 +2123,31 @@ void AgentBase::handle_swaig_request(const httplib::Request& req, httplib::Respo args = body["argument"]["parsed"][0]; } - // Check the security token if the tool is secure. The token travels on the - // QUERY STRING as ``__token`` (with ``token`` accepted as the reference's - // fallback spelling) — reference agent_base.py:1414 - // ``request.query_params.get("__token") or request.query_params.get("token")``. - // It is the same value build_swaig_functions appended to this tool's - // ``web_hook_url`` at render time. (``meta_data_token`` is a DIFFERENT wire - // field: the SWML ``UserSWAIGFunction`` meta_data SCOPING token, not a - // credential — reading it here validated the wrong value.) - auto tool_it = tools_.find(func_name); - if (tool_it != tools_.end() && tool_it->second.secure) { - std::string token = req.get_param_value("__token"); - if (token.empty()) { + // Enforce `secure` through the transport-agnostic core every transport + // shares, so the HTTP endpoint and the serverless envelopes cannot drift + // apart. The credential travels on the QUERY STRING as ``__token`` (with + // ``token`` accepted as the fallback spelling) — the same value + // build_swaig_functions appended to this tool's ``web_hook_url`` at render + // time. The ``call_id`` travels in the POST BODY. (``meta_data_token`` is a + // DIFFERENT wire field: the SWML ``UserSWAIGFunction`` meta_data SCOPING + // token, not a credential — reading it here validated the wrong value.) + // + // A refusal is a 200 + FunctionResult body, NOT an HTTP error status: the + // engine has no handling for a refusal status, so a non-200 would be dropped + // rather than relayed to the caller as "I cannot execute this action". + { + std::optional token; + if (req.has_param("__token")) { + token = req.get_param_value("__token"); + } else if (req.has_param("token")) { token = req.get_param_value("token"); } - std::string call_id = body.value("call_id", ""); - if (!session_manager_.validate_token(token, func_name, call_id)) { - res.status = 403; - res.set_content("{\"error\":\"invalid or expired token\"}", "application/json"); + std::optional call_id; + if (body.contains("call_id") && body["call_id"].is_string()) { + call_id = body["call_id"].get(); + } + if (auto refusal = swaig_validate_token(func_name, token, call_id)) { + res.set_content(refusal->to_string(), "application/json"); return; } } @@ -2068,6 +2189,37 @@ void AgentBase::handle_post_prompt_request(const httplib::Request& req, httplib: res.set_content("{\"status\":\"ok\"}", "application/json"); } +void AgentBase::handle_debug_events_request(const httplib::Request& req, httplib::Response& res) { + // Mirrors the reference's ``_handle_debug_events_request`` (web_mixin.py:1131): + // auth-checked, POST-only, JSON body, structured-log the event, 200 {"status":"ok"}. + add_security_headers(res); + if (!validate_auth(req, res)) { + return; + } + + json body; + try { + body = json::parse(req.body); + } catch (...) { + res.status = 400; + res.set_content("{\"error\":\"invalid JSON\"}", "application/json"); + return; + } + + // ``label`` then ``action``, defaulting to "unknown" — the reference's + // ``body.get("label") or body.get("action", "unknown")``. + std::string event_type = "unknown"; + if (body.contains("label") && body["label"].is_string() && + !body["label"].get().empty()) { + event_type = body["label"].get(); + } else if (body.contains("action") && body["action"].is_string()) { + event_type = body["action"].get(); + } + get_logger().info("debug_event event_type=" + event_type); + + res.set_content("{\"status\":\"ok\"}", "application/json"); +} + // ============================================================================ // Route Setup // ============================================================================ @@ -2161,6 +2313,16 @@ void AgentBase::setup_routes(httplib::Server& server) { handle_post_prompt_request(req, res); })); + // Debug-event webhook endpoint. Mounted when enable_debug_events() has been + // called — the same condition that puts params.debug_webhook_url on the wire, + // so the URL we advertise always resolves. + if (debug_events_enabled_) { + std::string de_path = base + (base.back() == '/' ? "" : "/") + "debug_events"; + server.Post(de_path, wrap_post([this](const httplib::Request& req, httplib::Response& res) { + handle_debug_events_request(req, res); + })); + } + // MCP server endpoint (JSON-RPC 2.0) if (mcp_server_enabled_) { std::string mcp_path = base + (base.back() == '/' ? "" : "/") + "mcp"; @@ -2217,10 +2379,15 @@ void AgentBase::setup_routes(httplib::Server& server) { void AgentBase::serve() { init_auth(); - // TLS termination in-process when SWML_SSL_ENABLED + cert/key are set - // (mirrors Python's SecurityConfig). SSLServer upcasts into the existing - // unique_ptr; setup_routes() is unchanged. - auto tls = server::resolve_tls_config_from_env(); + // TLS termination in-process, driven by the inherited swml::Service TLS + // values (seeded in its ctor from SecurityConfig — so SWML_SSL_ENABLED / + // SWML_SSL_CERT_PATH / SWML_SSL_KEY_PATH and any config file still apply — + // and overridable via set_ssl_*()). SSLServer upcasts into the existing + // shared_ptr; setup_routes() is unchanged. + server::TlsServerConfig tls; + tls.enabled = ssl_enabled(); + tls.cert_path = ssl_cert_path().value_or(""); + tls.key_path = ssl_key_path().value_or(""); // Build + configure under the lock, then listen() with our OWN strong // reference and the lock released: a concurrent stop() must be able to diff --git a/src/contexts/contexts.cpp b/src/contexts/contexts.cpp index c39b2c8..091bbe8 100644 --- a/src/contexts/contexts.cpp +++ b/src/contexts/contexts.cpp @@ -39,7 +39,8 @@ const std::set& reserved_native_tool_names() { // ============================================================================ GatherQuestion::GatherQuestion(const std::string& key, const std::string& question, - const std::string& type, bool confirm, const std::string& prompt, + const std::string& type, bool confirm, + const std::optional& prompt, const std::vector& functions, const std::optional& isolated) : key_(key), @@ -58,8 +59,10 @@ json GatherQuestion::to_json() const { if (confirm_) { j["confirm"] = true; } - if (!prompt_.empty()) { - j["prompt"] = prompt_; + // Reference guard is `if self.prompt:` — truthy, so an unset prompt AND an + // explicitly-empty one are both omitted. + if (prompt_.has_value() && !prompt_->empty()) { + j["prompt"] = *prompt_; } if (!functions_.empty()) { j["functions"] = functions_; @@ -75,8 +78,9 @@ json GatherQuestion::to_json() const { // GatherInfo // ============================================================================ -GatherInfo::GatherInfo(const std::string& output_key, const std::string& completion_action, - const std::string& prompt, bool isolated) +GatherInfo::GatherInfo(const std::optional& output_key, + const std::optional& completion_action, + const std::optional& prompt, bool isolated) : output_key_(output_key), completion_action_(completion_action), prompt_(prompt), @@ -84,7 +88,7 @@ GatherInfo::GatherInfo(const std::string& output_key, const std::string& complet GatherInfo& GatherInfo::add_question(const std::string& key, const std::string& question, const std::string& type, bool confirm, - const std::string& prompt, + const std::optional& prompt, const std::vector& functions, const std::optional& isolated) { questions_.emplace_back(key, question, type, confirm, prompt, functions, isolated); @@ -93,14 +97,16 @@ GatherInfo& GatherInfo::add_question(const std::string& key, const std::string& json GatherInfo::to_json() const { json j; - if (!output_key_.empty()) { - j["output_key"] = output_key_; + // Reference guards are truthy (`if self._output_key:`), so both an unset + // value and an explicitly-empty string are omitted. + if (output_key_.has_value() && !output_key_->empty()) { + j["output_key"] = *output_key_; } - if (!completion_action_.empty()) { - j["completion_action"] = completion_action_; + if (completion_action_.has_value() && !completion_action_->empty()) { + j["completion_action"] = *completion_action_; } - if (!prompt_.empty()) { - j["prompt"] = prompt_; + if (prompt_.has_value() && !prompt_->empty()) { + j["prompt"] = *prompt_; } if (!questions_.empty()) { j["questions"] = json::array(); @@ -180,14 +186,16 @@ Step& Step::set_history(const std::string& history) { return *this; } -Step& Step::set_gather_info(const std::string& output_key, const std::string& completion_action, - const std::string& prompt, bool isolated) { +Step& Step::set_gather_info(const std::optional& output_key, + const std::optional& completion_action, + const std::optional& prompt, bool isolated) { gather_info_ = GatherInfo(output_key, completion_action, prompt, isolated); return *this; } Step& Step::add_gather_question(const std::string& key, const std::string& question, - const std::string& type, bool confirm, const std::string& prompt, + const std::string& type, bool confirm, + const std::optional& prompt, const std::vector& functions, const std::optional& isolated) { if (gather_info_) { @@ -678,10 +686,13 @@ void ContextBuilder::validate() const { if (!gi_opt.has_value()) { continue; } - const auto& action = gi_opt->completion_action(); - if (action.empty()) { + const auto& action_opt = gi_opt->completion_action(); + // Reference guard is `if action is not None:` — an explicitly-set value + // is validated even when empty; only absence skips. + if (!action_opt.has_value()) { continue; } + const std::string& action = *action_opt; if (action == "next_step") { if (i + 1 >= order.size()) { @@ -699,7 +710,7 @@ void ContextBuilder::validate() const { "', " "(2) set completion_action to the name of an " "existing step in this context to jump to it, or " - "(3) leave completion_action empty (default) to " + "(3) set completion_action=nullopt (default) to " "stay in '"; msg += step_name; msg += @@ -734,7 +745,7 @@ void ContextBuilder::validate() const { msg += "' is not a step in this context. " "Valid options: 'next_step' (advance to the next " - "sequential step), empty string (stay in the current " + "sequential step), nullopt (stay in the current " "step), or one of "; msg += avail_str; msg += "."; diff --git a/src/core/auth_handler.cpp b/src/core/auth_handler.cpp index 9ea086a..19b518c 100644 --- a/src/core/auth_handler.cpp +++ b/src/core/auth_handler.cpp @@ -105,7 +105,9 @@ bool AuthHandler::bearer_ok(const Headers& headers) const { if (!starts_with(auth, "Bearer ")) { return false; } - return verify_bearer_token(BearerCredentials{auth.substr(7)}); + // Split the header the way FastAPI's HTTPBearer does: the scheme token and + // the credential string are carried as two separate fields. + return verify_bearer_token(BearerCredentials{"Bearer", auth.substr(7)}); } bool AuthHandler::api_key_ok(const Headers& headers) const { diff --git a/src/core/logging_config.cpp b/src/core/logging_config.cpp index b339e86..6a1aa9e 100644 --- a/src/core/logging_config.cpp +++ b/src/core/logging_config.cpp @@ -65,15 +65,18 @@ void configure_logging() { void reset_logging_configuration() { g_configured.store(false); } -bool get_logger(const std::string& /*name*/) { - // Single entry point: ensure the process logger is configured before use. +::signalwire::logging::Logger get_logger(const std::string& name) { + // Single entry point (the reference's contract): guarantee logging is + // configured, then hand back a NAMED logger so the caller can actually log + // AND `name` means something. Previously returned the configured-once bool and + // discarded `name`, so the canonical entry point could not produce a logger. if (!g_configured.load()) { configure_logging(); } - return g_configured.load(); + return ::signalwire::logging::get_logger(name); } -std::string strip_control_chars(const std::string& value) { +std::string strip_control_chars_str(const std::string& value) { std::string out; out.reserve(value.size()); for (char c : value) { @@ -88,6 +91,22 @@ std::string strip_control_chars(const std::string& value) { return out; } +nlohmann::json strip_control_chars(const nlohmann::json& event_dict) { + // Not an object (or empty): nothing to walk — return it unchanged, matching + // the reference's behaviour of only touching string VALUES of the event map. + if (!event_dict.is_object()) { + return event_dict; + } + nlohmann::json out = event_dict; + for (auto& [key, value] : out.items()) { + (void)key; + if (value.is_string()) { + value = strip_control_chars_str(value.get()); + } + } + return out; +} + } // namespace logging_config } // namespace core } // namespace signalwire diff --git a/src/core/swml_builder.cpp b/src/core/swml_builder.cpp index e51ebc9..1213d48 100644 --- a/src/core/swml_builder.cpp +++ b/src/core/swml_builder.cpp @@ -9,6 +9,12 @@ namespace signalwire { namespace core { +// Every verb below goes through ``service_.add_verb(name, config)`` — the +// VALIDATING entry point — never ``service_.document().add_verb(...)``, which +// performs no schema check at all. The builder rode the raw path until task +// #194; that is why a schema-forbidden config (a `play` `text` key, a `hangup` +// `reason` outside the closed hangup|busy|decline enum) could be built here and +// shipped. A caller now gets a ``SchemaValidationError`` at build time. SWMLBuilder::SWMLBuilder(swml::Service& service) : service_(service) {} SWMLBuilder& SWMLBuilder::answer(std::optional max_duration, @@ -20,7 +26,7 @@ SWMLBuilder& SWMLBuilder::answer(std::optional max_duration, if (codecs.has_value()) { config["codecs"] = *codecs; } - service_.document().add_verb("answer", config); + service_.add_verb("answer", config); return *this; } @@ -29,7 +35,7 @@ SWMLBuilder& SWMLBuilder::hangup(std::optional reason) { if (reason.has_value()) { config["reason"] = *reason; } - service_.document().add_verb("hangup", config); + service_.add_verb("hangup", config); return *this; } @@ -62,7 +68,7 @@ SWMLBuilder& SWMLBuilder::ai(std::optional prompt_text, std::option } } - service_.document().add_verb("ai", config); + service_.add_verb("ai", config); return *this; } @@ -98,7 +104,7 @@ SWMLBuilder& SWMLBuilder::play(std::optional url, config["auto_answer"] = *auto_answer; } - service_.document().add_verb("play", config); + service_.add_verb("play", config); return *this; } diff --git a/src/core/swml_renderer.cpp b/src/core/swml_renderer.cpp index beff563..261a0fb 100644 --- a/src/core/swml_renderer.cpp +++ b/src/core/swml_renderer.cpp @@ -53,8 +53,10 @@ std::string SwmlRenderer::render_swml(const json& prompt, swml::Service& service } if (opts.record_call) { - service.document().add_verb("record_call", json::object({{"format", opts.record_format}, - {"stereo", opts.record_stereo}})); + // Through service.add_verb, not service.document().add_verb: the Document + // entry point performs no schema check at all (task #194). + service.add_verb("record_call", json::object({{"format", opts.record_format}, + {"stereo", opts.record_stereo}})); } // Assemble the SWAIG function list: startup/hangup hooks first, then the @@ -116,8 +118,14 @@ std::string SwmlRenderer::render_function_response_swml( // Reset the document to start fresh. service.document() = swml::Document(); + // Text is played via the `say:` URL scheme — the SWML `play` verb has no + // `text` key (schema.json: oneOf[PlayWithURL, PlayWithURLS] with + // `unevaluatedProperties: {"not": {}}`), so `{"text": ...}` produces a + // document the schema rejects. The canonical form, which SWMLBuilder::play / + // ::say next door already use, is `url: "say:"`. Matches the reference + // (swml_renderer.py: `service.add_verb("play", {"url": f"say:{response_text}"})`). if (!response_text.empty()) { - service.document().add_verb("play", json::object({{"text", response_text}})); + service.add_verb("play", json::object({{"url", "say:" + response_text}})); } if (actions.has_value()) { @@ -125,15 +133,18 @@ std::string SwmlRenderer::render_function_response_swml( if (!action.is_object()) { continue; } - // First recognized action verb wins (precedence order). + // First recognized action verb wins (precedence order). These configs come + // straight from a SWAIG function's caller-supplied result, so they are the + // shapes MOST in need of validation — service.add_verb, never + // service.document().add_verb (task #194). if (action.contains("play")) { - service.document().add_verb("play", action.at("play")); + service.add_verb("play", action.at("play")); } else if (action.contains("hangup")) { - service.document().add_verb("hangup", action.at("hangup")); + service.add_verb("hangup", action.at("hangup")); } else if (action.contains("transfer")) { - service.document().add_verb("transfer", action.at("transfer")); + service.add_verb("transfer", action.at("transfer")); } else if (action.contains("ai")) { - service.document().add_verb("ai", action.at("ai")); + service.add_verb("ai", action.at("ai")); } } } diff --git a/src/datamap/datamap.cpp b/src/datamap/datamap.cpp index 0d3af2e..70c7d2f 100644 --- a/src/datamap/datamap.cpp +++ b/src/datamap/datamap.cpp @@ -2,6 +2,9 @@ // SPDX-License-Identifier: MIT #include "signalwire/datamap/datamap.hpp" +#include +#include + namespace signalwire { namespace datamap { @@ -49,7 +52,9 @@ DataMap& DataMap::expression(const std::string& test_value, const std::string& p expr["pattern"] = pattern; expr["output"] = output_result.to_json(); if (nomatch_output) { - expr["nomatch_output"] = nomatch_output->to_json(); + // HYPHENATED wire key per the reference (data_map.py:202). An underscore is a + // key the server does not recognise, so the no-match branch would never fire. + expr["nomatch-output"] = nomatch_output->to_json(); } expressions_.push_back(expr); return *this; @@ -59,7 +64,13 @@ DataMap& DataMap::webhook(const std::string& method, const std::string& url, con const std::string& form_param, bool input_args_as_params, const std::vector& require_args) { json wh; - wh["method"] = method; + // The reference upper-cases the method on the wire (core/data_map.py:230, + // `"method": method.upper()`), so the same program emits byte-identical SWML in + // both languages. The engine itself compares case-insensitively. + std::string upper_method = method; + std::transform(upper_method.begin(), upper_method.end(), upper_method.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + wh["method"] = upper_method; wh["url"] = url; if (!headers.empty()) { wh["headers"] = headers; @@ -84,14 +95,6 @@ DataMap& DataMap::webhook_expressions(const std::vector& expressions) { return *this; } -DataMap& DataMap::body(const json& data) { - if (webhooks_.empty()) { - throw std::runtime_error("Must add webhook before setting body"); - } - webhooks_.back()["body"] = data; - return *this; -} - DataMap& DataMap::params(const json& data) { if (webhooks_.empty()) { throw std::runtime_error("Must add webhook before setting params"); diff --git a/src/pom/pom.cpp b/src/pom/pom.cpp index 1409ebd..88f0daf 100644 --- a/src/pom/pom.cpp +++ b/src/pom/pom.cpp @@ -38,12 +38,15 @@ void Section::add_bullets(const std::vector& bs) { } Section& Section::add_subsection(const std::string& t, const std::string& b, - const std::vector& bs, std::optional num, + const std::vector& bs, bool num, bool numbered_bullets) { if (t.empty()) { throw std::invalid_argument("Subsections must have a title"); } - subsections.emplace_back(std::optional(t), b, bs, num, numbered_bullets); + // Reference passes ``numbered`` through verbatim, so the stored value is the + // explicit bool — never the tri-state ``None``. + subsections.emplace_back(std::optional(t), b, bs, std::optional(num), + numbered_bullets); return subsections.back(); } diff --git a/src/prefabs/info_gatherer.cpp b/src/prefabs/info_gatherer.cpp index 5e2063e..b294950 100644 --- a/src/prefabs/info_gatherer.cpp +++ b/src/prefabs/info_gatherer.cpp @@ -91,18 +91,30 @@ InfoGathererAgent& InfoGathererAgent::set_question_callback(QuestionCallback cb) return *this; } -json InfoGathererAgent::on_swml_request(const json& request_data, const json& query_params, - const json& headers) { +std::optional InfoGathererAgent::on_swml_request( + const std::optional& request_data, const std::optional& /*callback_path*/, + const std::optional& request) { // Static mode: no dynamic override. if (has_static_questions_) { - return json(); // null + return std::nullopt; } if (!question_callback_) { return global_data_override(fallback_questions()); } - json qp = query_params.is_object() ? query_params : json::object(); - json bp = request_data.is_object() ? request_data : json::object(); - json hd = headers.is_object() ? headers : json::object(); + // Reference: query_params/headers are read OFF the request object and default + // to {} when it (or the attribute) is absent; body_params is `request_data or {}`. + json qp = json::object(); + json hd = json::object(); + if (request.has_value() && request->is_object()) { + if (request->contains("query_params") && (*request)["query_params"].is_object()) { + qp = (*request)["query_params"]; + } + if (request->contains("headers") && (*request)["headers"].is_object()) { + hd = (*request)["headers"]; + } + } + json bp = + (request_data.has_value() && request_data->is_object()) ? *request_data : json::object(); try { std::vector questions = question_callback_(qp, bp, hd); if (questions.empty()) { diff --git a/src/relay/call.cpp b/src/relay/call.cpp index 7f5eba6..e7d9984 100644 --- a/src/relay/call.cpp +++ b/src/relay/call.cpp @@ -232,10 +232,16 @@ Action Call::denoise() { return execute_simple("denoise"); } Action Call::denoise_stop() { return execute_simple("denoise.stop"); } Action Call::bind_digit(const std::string& digits, const std::string& bind_method, - const json& params) { + const json& params, const std::optional& bind_params) { json p = params.is_object() ? params : json::object(); p["digits"] = digits; p["bind_method"] = bind_method; + // The reference's ``bind_params`` API name lands on the WIRE key ``params`` + // (relay/call.py:1359), and rides only when supplied — the reference's guard + // is ``if bind_params is not None``, so nullopt omits the key entirely. + if (bind_params.has_value()) { + p["params"] = *bind_params; + } return execute_simple("bind_digit", p); } @@ -262,10 +268,10 @@ Action Call::queue_leave(const std::string& queue_name, const json& params) { } Action Call::leave_conference(const std::string& conference_id) { + // Reference (relay/call.py:1264) declares conference_id REQUIRED and always + // puts it in params — there is no absence guard and no default to omit it. json p; - if (!conference_id.empty()) { - p["conference_id"] = conference_id; - } + p["conference_id"] = conference_id; return execute_simple("leave_conference", p); } @@ -286,8 +292,14 @@ Action Call::ai_message(const json& params) { return execute_simple("ai_message", p); } -Action Call::amazon_bedrock(const json& params) { +Action Call::amazon_bedrock(const json& params, const std::optional& ai_params) { json p = params.is_object() ? params : json::object(); + // The reference's ``ai_params`` API name lands on the WIRE key ``params`` + // (relay/call.py:1502), riding only when supplied (reference guard: + // ``if ai_params is not None``). + if (ai_params.has_value()) { + p["params"] = *ai_params; + } // RULES §4: a Bedrock engine routes to the dedicated calling.amazon_bedrock // RPC, NOT calling.ai. execute_simple prepends "calling." so we pass the // bare wire method name. @@ -388,11 +400,6 @@ Action Call::record(const json& params, const std::string& control_id) { Action Call::record_call(const json& params) { return record(params); } -Action Call::prompt(const json& play_media, const json& collect_params, - const std::string& control_id) { - return play_and_collect(play_media, collect_params, control_id); -} - Action Call::play_and_collect(const json& play_media, const json& collect_params, const std::string& control_id) { json p; diff --git a/src/relay/client.cpp b/src/relay/client.cpp index 2b53a2e..a7f3d97 100644 --- a/src/relay/client.cpp +++ b/src/relay/client.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -99,6 +100,27 @@ bool RelayClient::open_ws_transport() { host = host.substr(0, colon); } if (scheme == "ws" || scheme == "ws://") { + // NO SILENT DOWNGRADE. Setting SIGNALWIRE_RELAY_CA_FILE is an explicit + // request to VERIFY the RELAY peer against that CA — which is meaningless + // without TLS. If the transport also resolved to plain ws:// (a stale + // SIGNALWIRE_RELAY_SCHEME, a harness export leaking out of a test run, an + // operator who changed one setting and not the other), connect_plain() + // would happily complete a PLAINTEXT session and authenticate over it: the + // caller asked for encryption, got none, and was never told. Refuse, and + // name the setting that would otherwise have been silently ignored. + // Plaintext WITHOUT the CA var is untouched — that is a deliberate, + // unambiguous request for a clear connection (the audit fixture / dev + // servers), and it still works exactly as before. + // Matches the guard signalwire-rust ships in src/relay/client.rs. + const char* relay_ca = std::getenv("SIGNALWIRE_RELAY_CA_FILE"); + if (relay_ca != nullptr && *relay_ca != '\0') { + get_logger().error( + "SIGNALWIRE_RELAY_CA_FILE is set (TLS verification requested) but the RELAY " + "endpoint resolved to plaintext ws:// — refusing to downgrade. Use the wss:// " + "transport (check SIGNALWIRE_RELAY_SCHEME), or unset SIGNALWIRE_RELAY_CA_FILE " + "to connect in the clear deliberately."); + return false; + } return ws_->connect_plain(host, port); } return ws_->connect(host, port); @@ -761,8 +783,8 @@ json RelayClient::send_raw_request(const std::string& method, const json& params return send_request(method, params); } -Call RelayClient::dial(const json& devices, const std::string& tag_in, int dial_timeout_ms, - int max_duration) { +Call RelayClient::dial(const json& devices, const std::string& tag_in, int max_duration, + std::optional dial_timeout) { std::string tag = tag_in.empty() ? generate_uuid() : tag_in; // Register pending dial before sending RPC @@ -790,8 +812,11 @@ Call RelayClient::dial(const json& devices, const std::string& tag_in, int dial_ return Call(); } - // Wait for the dial event (with timeout) - auto status = future.wait_for(std::chrono::milliseconds(dial_timeout_ms)); + // Wait for the dial event. dial_timeout is in SECONDS (reference unit), and + // absent means 120s — the reference's + // `timeout = dial_timeout if dial_timeout is not None else 120.0`. + const double timeout_s = dial_timeout.value_or(120.0); + auto status = future.wait_for(std::chrono::duration(timeout_s)); { std::lock_guard lock(dials_mutex_); pending_dials_.erase(tag); diff --git a/src/security/session_manager.cpp b/src/security/session_manager.cpp index 25d386f..8fcd88d 100644 --- a/src/security/session_manager.cpp +++ b/src/security/session_manager.cpp @@ -16,12 +16,11 @@ namespace signalwire { namespace security { namespace { -/// Truncate to 8 chars + "..." when longer (matches the authoritative -/// reference's debug_token component redaction). +/// Truncate to 8 chars + "..." when longer — the redaction ``debug_token`` +/// applies to each token component. std::string truncate8(const std::string& s) { return s.size() > 8 ? s.substr(0, 8) + "..." : s; } -/// Format a Unix timestamp as an ISO-8601 UTC string (matches Python's -/// datetime.fromtimestamp(...).isoformat() closely enough for the debug view). +/// Format a Unix timestamp as an ISO-8601 UTC string, for the debug view. std::string iso8601_utc(int64_t ts) { std::time_t t = static_cast(ts); std::tm tm_buf{}; @@ -117,9 +116,13 @@ std::string SessionManager::base64url_encode(const std::string& data) { c = '_'; } } - while (!out.empty() && out.back() == '=') { - out.pop_back(); - } + // PADDING IS KEPT, deliberately. The reference mints with + // ``base64.urlsafe_b64encode``, which pads, and validates with + // ``base64.urlsafe_b64decode``, which RAISES on a stripped '='. Popping the + // '=' here made every minted token unusable to the reference (and to any port + // that decodes strictly) even though the message and HMAC were correct, while + // our own ``base64url_decode`` still accepted them because it re-pads — the + // asymmetry that makes this class of break invisible to a self round-trip. return out; } diff --git a/src/security/webhook_validator.cpp b/src/security/webhook_validator.cpp index 6f55555..d5a1000 100644 --- a/src/security/webhook_validator.cpp +++ b/src/security/webhook_validator.cpp @@ -229,9 +229,7 @@ std::string build_url(const ParsedUrl& p, const std::string& port_override) { return out; } -/// Return the URL variants to try for Scheme B port normalization. -/// -/// Mirrors ``_candidate_urls`` in the Python reference: +/// Return the URL variants to try for Scheme B port normalization: /// - non-standard explicit port -> just the input URL /// - https + no port -> input + url with :443 /// - http + no port -> input + url with :80 diff --git a/src/server/tls_server.hpp b/src/server/tls_server.hpp index 0469d28..3550df4 100644 --- a/src/server/tls_server.hpp +++ b/src/server/tls_server.hpp @@ -36,9 +36,8 @@ struct TlsServerConfig { bool usable() const { return enabled && !cert_path.empty() && !key_path.empty(); } }; -/// Resolve TLS config from the SWML_SSL_* environment variables, mirroring -/// signalwire-python's SecurityConfig.load_from_env(). Returns enabled=false -/// when SWML_SSL_ENABLED is unset/false. +/// Resolve TLS config from the SWML_SSL_* environment variables. Returns +/// enabled=false when SWML_SSL_ENABLED is unset/false. inline TlsServerConfig resolve_tls_config_from_env() { TlsServerConfig cfg; std::string enabled = get_env("SWML_SSL_ENABLED", ""); diff --git a/src/skills/builtin/api_ninjas_trivia.cpp b/src/skills/builtin/api_ninjas_trivia.cpp index d802f3f..493b59d 100644 --- a/src/skills/builtin/api_ninjas_trivia.cpp +++ b/src/skills/builtin/api_ninjas_trivia.cpp @@ -22,7 +22,7 @@ class ApiNinjasTriviaSkill : public SkillBase { std::vector register_tools() override { return {}; } - /// Corresponds to ``get_tools`` — the SWAIG tool defs this skill contributes. + /// The SWAIG tool defs this skill contributes. std::vector get_tools() const { return get_datamap_functions(); } std::vector get_datamap_functions() const override { diff --git a/src/skills/builtin/datasphere.cpp b/src/skills/builtin/datasphere.cpp index fc7acb5..ae91319 100644 --- a/src/skills/builtin/datasphere.cpp +++ b/src/skills/builtin/datasphere.cpp @@ -10,11 +10,16 @@ namespace signalwire { namespace skills { -/// SignalWire DataSphere RAG search skill — issues a real POST against -/// the DataSphere `/api/datasphere/documents/{document_id}/search` endpoint -/// with the user query in the JSON body, parses the `results[]` array, -/// and returns a flattened text summary. Matches the Python -/// `DatasphereSkill` upstream-call shape. +/// SignalWire DataSphere RAG search skill — issues a real POST against the +/// DataSphere `/api/datasphere/documents/search` COLLECTION endpoint. The +/// `document_id` travels in the JSON body alongside `query_string`, `count` +/// and `distance`, with Basic auth derived from `project_id:token`. +/// +/// Wire shape: the URL is `.../api/datasphere/documents/search` with NO +/// per-document path segment, the payload carries `document_id`, and the +/// response array is **`chunks`** — NOT `results`, which is the easy mistake +/// here. Rendering emits an "I found N result(s) for 'q'" header, then +/// `=== RESULT n ===` blocks reading `text` → `content` → `chunk` → raw JSON. /// /// `DATASPHERE_BASE_URL` env var overrides the upstream URL (used by /// `audit_skills_dispatch.py`); when unset, the real upstream is built @@ -38,6 +43,9 @@ class DatasphereSkill : public SkillBase { doc_id_ = get_param(params, "document_id", ""); tool_name_ = get_param(params, "tool_name", "search_knowledge"); count_ = get_param(params, "count", 1); + // Reference: ``self.distance = self.params.get("distance", 3.0)`` — sent on + // every search payload. + distance_ = get_param(params, "distance", 3.0); return !space_.empty() && !project_id_.empty() && !token_.empty(); } @@ -63,16 +71,20 @@ class DatasphereSkill : public SkillBase { if (base.empty()) { base = "https://" + space_ + ".signalwire.com"; } - std::string url = base + "/api/datasphere/documents/" + url_encode(doc_id_) + "/search"; + // COLLECTION path — the document_id goes in the BODY, not the URL. + std::string url = base + "/api/datasphere/documents/search"; json body = json::object({ + {"document_id", doc_id_}, {"query_string", query}, + {"distance", distance_}, {"count", count_}, }); std::map headers; std::string basic = base64_encode(project_id_ + ":" + token_); headers["Authorization"] = "Basic " + basic; + headers["Accept"] = "application/json"; auto resp = http_post(url, body.dump(), "application/json", headers); if (resp.status == 0) { @@ -90,11 +102,30 @@ class DatasphereSkill : public SkillBase { return swaig::FunctionResult(std::string("DataSphere parse error: ") + e.what()); } + // The upstream returns ``chunks``, not ``results`` (reference says so + // in as many words). An absent/empty array is the no-results path. + if (!parsed.contains("chunks") || !parsed["chunks"].is_array()) { + return swaig::FunctionResult("No results found for '" + query + "'"); + } + const auto& chunks = parsed["chunks"]; + if (chunks.empty()) { + return swaig::FunctionResult("No results found for '" + query + "'"); + } + std::ostringstream out; - out << "DataSphere results for '" << query << "':\n"; - if (parsed.contains("results") && parsed["results"].is_array()) { - for (const auto& r : parsed["results"]) { - out << "- " << r.value("text", "") << "\n"; + out << "I found " << chunks.size() << (chunks.size() == 1 ? " result" : " results") + << " for '" << query << "':\n\n"; + int idx = 1; + for (const auto& c : chunks) { + out << "=== RESULT " << idx++ << " ===\n"; + if (c.contains("text") && c["text"].is_string()) { + out << c["text"].get() << "\n"; + } else if (c.contains("content") && c["content"].is_string()) { + out << c["content"].get() << "\n"; + } else if (c.contains("chunk") && c["chunk"].is_string()) { + out << c["chunk"].get() << "\n"; + } else { + out << c.dump() << "\n"; } } return swaig::FunctionResult(out.str()); @@ -117,6 +148,7 @@ class DatasphereSkill : public SkillBase { private: std::string space_, project_id_, token_, doc_id_, tool_name_; int count_ = 1; + double distance_ = 3.0; }; REGISTER_SKILL(DatasphereSkill) diff --git a/src/skills/builtin/datasphere_serverless.cpp b/src/skills/builtin/datasphere_serverless.cpp index 78efe9a..c9528c1 100644 --- a/src/skills/builtin/datasphere_serverless.cpp +++ b/src/skills/builtin/datasphere_serverless.cpp @@ -23,6 +23,8 @@ class DatasphereServerlessSkill : public SkillBase { token_ = get_param_or_env(params, "token", "SIGNALWIRE_API_TOKEN"); doc_id_ = get_param(params, "document_id", ""); tool_name_ = get_param(params, "tool_name", "search_knowledge"); + count_ = get_param(params, "count", 1); + distance_ = get_param(params, "distance", 3.0); return !space_.empty() && !project_id_.empty() && !token_.empty(); } @@ -38,7 +40,15 @@ class DatasphereServerlessSkill : public SkillBase { .webhook("POST", url, json::object( {{"Content-Type", "application/json"}, {"Authorization", "Basic " + auth}})) - .body(json::object({{"query", "${args.query}"}, {"document_id", doc_id_}, {"count", 1}})) + .params(json::object({{"document_id", doc_id_}, + {"query_string", "${args.query}"}, + {"count", count_}, + {"distance", distance_}})) + .foreach (json::object( + {{"input_key", "chunks"}, + {"output_key", "formatted_results"}, + {"max", count_}, + {"append", "=== RESULT ===\n${this.text}\n" + std::string(50, '=') + "\n\n"}})) .output(swaig::FunctionResult( "I found results for \"${args.query}\":\n\n${formatted_results}")); @@ -59,6 +69,8 @@ class DatasphereServerlessSkill : public SkillBase { private: std::string space_, project_id_, token_, doc_id_, tool_name_; + int count_ = 1; + double distance_ = 3.0; }; REGISTER_SKILL(DatasphereServerlessSkill) diff --git a/src/skills/builtin/mcp_gateway.cpp b/src/skills/builtin/mcp_gateway.cpp index e45fd54..10baf3c 100644 --- a/src/skills/builtin/mcp_gateway.cpp +++ b/src/skills/builtin/mcp_gateway.cpp @@ -1,5 +1,6 @@ // Copyright (c) 2025 SignalWire // SPDX-License-Identifier: MIT +#include "httplib.h" #include "signalwire/skills/skill_base.hpp" #include "signalwire/skills/skill_registry.hpp" @@ -13,30 +14,63 @@ class McpGatewaySkill : public SkillBase { return "Bridge MCP servers with SWAIG functions"; } + // Advertise the configurable parameters (mirrors the Python skill's + // MCPGatewaySkill.get_parameter_schema). ``verify_ssl`` defaults to TRUE so a + // gateway call verifies the server certificate unless the operator explicitly + // opts out. + json get_parameter_schema() const override { + return json::object( + {{"gateway_url", json::object({{"type", "string"}, + {"description", "URL of the MCP Gateway service"}, + {"required", true}})}, + {"tool_prefix", + json::object({{"type", "string"}, + {"description", "Prefix for registered SWAIG function names"}, + {"default", "mcp_"}, + {"required", false}})}, + {"request_timeout", json::object({{"type", "integer"}, + {"description", "Request timeout in seconds"}, + {"default", 30}, + {"required", false}})}, + {"verify_ssl", json::object({{"type", "boolean"}, + {"description", "Verify SSL certificates"}, + {"default", true}, + {"required", false}})}}); + } + bool setup(const json& params) override { params_ = params; gateway_url_ = get_param(params, "gateway_url", ""); tool_prefix_ = get_param(params, "tool_prefix", "mcp_"); + request_timeout_ = get_param(params, "request_timeout", 30); + // Secure default: verify SSL certificates unless the operator opts out. + verify_ssl_ = get_param(params, "verify_ssl", true); return !gateway_url_.empty(); } std::vector register_tools() override { - // In full implementation, this would connect to the MCP gateway - // and dynamically create tools based on discovered services std::vector tools; if (params_.contains("services") && params_["services"].is_array()) { for (const auto& svc : params_["services"]) { std::string svc_name = svc.value("name", "service"); + // Capture the config the handler needs to reach the gateway, INCLUDING + // verify_ssl, so each call verifies the cert per the skill's setting. + std::string gateway_url = gateway_url_; + bool verify_ssl = verify_ssl_; + int timeout = request_timeout_; tools.push_back(define_tool( tool_prefix_ + svc_name + "_query", "[" + svc_name + "] Query the MCP service", json::object({{"type", "object"}, {"properties", json::object({{"query", json::object({{"type", "string"}, {"description", "Query"}})}})}}), - [this, svc_name](const json& args, const json&) -> swaig::FunctionResult { - return swaig::FunctionResult("MCP gateway query to " + svc_name + " via " + - gateway_url_); + [gateway_url, svc_name, verify_ssl, timeout](const json& args, + const json&) -> swaig::FunctionResult { + json body = json::object({{"tool", svc_name}, {"arguments", args}}); + std::string result = call_gateway(gateway_url, "/services/" + svc_name + "/call", + body, verify_ssl, timeout); + return swaig::FunctionResult(result); })); } } @@ -77,9 +111,58 @@ class McpGatewaySkill : public SkillBase { bullets}}; } + // Expose the cert-verification setting for tests / callers. This is the value + // wired into ``enable_server_certificate_verification`` on every gateway call. + bool verify_ssl() const { return verify_ssl_; } + private: + // POST a JSON body to the gateway. ``verify_ssl`` controls TLS server-cert + // verification: when true (the secure default) the https client verifies the + // server certificate; when the operator set verify_ssl=false it is turned off. + // This is the real wiring — the flag drives httplib's certificate check, not a + // stored no-op. + static std::string call_gateway(const std::string& gateway_url, const std::string& path, + const json& body, bool verify_ssl, int timeout_seconds) { + // Split scheme+host from the base url; httplib::Client takes the origin and + // the request path separately. + std::string origin = gateway_url; + // Strip any trailing slash so path concatenation is clean. + while (!origin.empty() && origin.back() == '/') { + origin.pop_back(); + } + httplib::Client cli(origin); + // WIRED: verify_ssl (default true) drives httplib server-cert verification. + cli.enable_server_certificate_verification(verify_ssl); + auto micros = std::chrono::microseconds(static_cast(timeout_seconds) * 1000000LL); + cli.set_connection_timeout(micros); + cli.set_read_timeout(micros); + cli.set_write_timeout(micros); + // Honor an explicit CA bundle if the fleet var is set (same trust bundle the + // REST client reads), keeping verification enabled per verify_ssl above. + if (const char* rest_ca = std::getenv("SIGNALWIRE_REST_CA_FILE")) { + if (rest_ca && *rest_ca) { + cli.set_ca_cert_path(rest_ca); + } + } + auto res = cli.Post(path, body.dump(), "application/json"); + if (res && res->status == 200) { + try { + json parsed = json::parse(res->body); + return parsed.value("result", std::string("No response")); + } catch (const std::exception&) { + return res->body; + } + } + if (res) { + return "MCP gateway error: HTTP " + std::to_string(res->status); + } + return "MCP gateway connection error"; + } + std::string gateway_url_; std::string tool_prefix_ = "mcp_"; + int request_timeout_ = 30; + bool verify_ssl_ = true; }; REGISTER_SKILL(McpGatewaySkill) diff --git a/src/skills/builtin/native_vector_search.cpp b/src/skills/builtin/native_vector_search.cpp index 0627fe2..53b8bb2 100644 --- a/src/skills/builtin/native_vector_search.cpp +++ b/src/skills/builtin/native_vector_search.cpp @@ -65,8 +65,8 @@ class NativeVectorSearchSkill : public SkillBase { } private: - /// Network mode (Python _search_remote): POST {query,index_name,count,...} to - /// /search and format the returned results into the tool result. + /// Network mode: POST {query,index_name,count,...} to /search + /// and format the returned results into the tool result. swaig::FunctionResult search_remote(const std::string& query, int count) const { json request = json::object({ {"query", query}, diff --git a/src/skills/builtin/play_background_file.cpp b/src/skills/builtin/play_background_file.cpp index 1efb777..4776d0b 100644 --- a/src/skills/builtin/play_background_file.cpp +++ b/src/skills/builtin/play_background_file.cpp @@ -21,7 +21,7 @@ class PlayBackgroundFileSkill : public SkillBase { std::vector register_tools() override { return {}; } - /// Corresponds to ``get_tools`` — the SWAIG tool defs this skill contributes. + /// The SWAIG tool defs this skill contributes. std::vector get_tools() const { return get_datamap_functions(); } std::vector get_datamap_functions() const override { diff --git a/src/skills/builtin/spider.cpp b/src/skills/builtin/spider.cpp index 9b8dcb0..03ffdcc 100644 --- a/src/skills/builtin/spider.cpp +++ b/src/skills/builtin/spider.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: MIT #include #include +#include +#include #include "signalwire/common.hpp" #include "signalwire/skills/skill_base.hpp" @@ -13,20 +15,58 @@ namespace skills { namespace { -/// Strip HTML tags from `html` and collapse repeated whitespace. Matches the -/// "naive HTML strip" Python's spider skill does for its scrape_url tool. -std::string strip_html(const std::string& html) { +/// Drop each element matched by `xpaths` — tag AND its inner content — +/// before any tag-stripping runs, so a ``" + "" + "" + "
secret_header_token
" + "" + "" + "

keeper body text

" + "
secret_footer_token
" + "", + "text/html"); + }); + + int port = 0; + std::thread th([&] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ASSERT_TRUE(port > 0); + + ::setenv("SPIDER_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + auto skill = sw_skills::SkillRegistry::instance().create("spider"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_TRUE(!tools.empty()); + auto result = + tools[0].handler(json::object({{"url", "https://example.com/page"}}), json::object()); + auto resp = result.to_json()["response"].get(); + + srv.stop(); + th.join(); + ::unsetenv("SPIDER_BASE_URL"); + + // Every default remove_xpaths entry: //script //style //nav //header //footer + // //aside //noscript — content dropped, not merely untagged. + ASSERT_TRUE(resp.find("secret_js_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_css_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_nav_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_header_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_aside_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_noscript_token") == std::string::npos); + ASSERT_TRUE(resp.find("secret_footer_token") == std::string::npos); + // …and the real body survives, so the fold is not just "drop everything". + ASSERT_TRUE(resp.find("keeper body text") != std::string::npos); + return true; +} + +// The surviving (and now ONLY) spider implementation is the one in +// ``src/skills/builtin/spider.cpp`` — the same file the surface enumerator +// reads. Before the duplicate in ``skill_registry.cpp`` was deleted, a +// ``remove_xpaths`` fix could land in builtin/ and have no effect at runtime, +// because the registry's ``SpiderSkillR`` was the class actually registered. +// Pin the identity here so a re-introduced duplicate is caught, and re-prove +// the strip behaviour end to end against the live registration. +TEST(skill_spider_live_impl_is_the_builtin_and_strips_script_and_nav) { + auto skill = sw_skills::SkillRegistry::instance().create("spider"); + ASSERT_TRUE(skill != nullptr); + // The deleted duplicate said "Web scraping"; the builtin (and the Python + // reference's SKILL_DESCRIPTION) says this. + ASSERT_EQ(skill->skill_description(), "Fast web scraping and crawling capabilities"); + + httplib::Server srv; + srv.Get("/page", [&](const httplib::Request&, httplib::Response& res) { + res.set_content( + "" + "" + "" + "

VISIBLE BODY

" + "", + "text/html"); + }); + + int port = 0; + std::thread th([&] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ASSERT_TRUE(port > 0); + + ::setenv("SPIDER_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_TRUE(!tools.empty()); + auto result = + tools[0].handler(json::object({{"url", "https://example.com/page"}}), json::object()); + auto resp = result.to_json()["response"].get(); + + srv.stop(); + th.join(); + ::unsetenv("SPIDER_BASE_URL"); + + ASSERT_TRUE(resp.find("alert(1)") == std::string::npos); // //script dropped + ASSERT_TRUE(resp.find("NAVTEXT") == std::string::npos); // //nav dropped + ASSERT_TRUE(resp.find("VISIBLE BODY") != std::string::npos); + return true; } diff --git a/tests/test_skill_transfer.cpp b/tests/test_skill_transfer.cpp index b581d22..fe6d9da 100644 --- a/tests/test_skill_transfer.cpp +++ b/tests/test_skill_transfer.cpp @@ -4,81 +4,75 @@ namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_transfer_name) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - ASSERT_EQ(skill->skill_name(), "swml_transfer"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_EQ(skill->skill_name(), "swml_transfer"); + return true; } TEST(skill_transfer_multi_instance) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - ASSERT_TRUE(skill->supports_multiple_instances()); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->supports_multiple_instances()); + return true; } TEST(skill_transfer_setup_requires_transfers) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - ASSERT_FALSE(skill->setup(json::object())); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_FALSE(skill->setup(json::object())); + return true; } TEST(skill_transfer_setup_with_transfers) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - ASSERT_TRUE(skill->setup(json::object({ - {"transfers", json::object({ - {"sales", json::object({{"url", "https://example.com/sales"}, {"message", "Transferring to sales"}})}, - {"support", json::object({{"url", "https://example.com/support"}})} - })} - }))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->setup(json::object( + {{"transfers", + json::object({{"sales", json::object({{"url", "https://example.com/sales"}, + {"message", "Transferring to sales"}})}, + {"support", json::object({{"url", "https://example.com/support"}})}})}}))); + return true; } TEST(skill_transfer_returns_datamap) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - skill->setup(json::object({ - {"transfers", json::object({ - {"sales", json::object({{"url", "https://example.com/sales"}})} - })} - })); - auto dm = skill->get_datamap_functions(); - ASSERT_EQ(dm.size(), 1u); - ASSERT_EQ(dm[0]["function"].get(), "transfer_call"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->setup(json::object( + {{"transfers", + json::object({{"sales", json::object({{"url", "https://example.com/sales"}})}})}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_EQ(dm.size(), 1u); + ASSERT_EQ(dm[0]["function"].get(), "transfer_call"); + return true; } TEST(skill_transfer_custom_tool_name) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - skill->setup(json::object({ - {"tool_name", "route_call"}, - {"transfers", json::object({{"dept", json::object({{"url", "x"}})}})} - })); - auto dm = skill->get_datamap_functions(); - ASSERT_EQ(dm[0]["function"].get(), "route_call"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->setup( + json::object({{"tool_name", "route_call"}, + {"transfers", json::object({{"dept", json::object({{"url", "x"}})}})}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_EQ(dm[0]["function"].get(), "route_call"); + return true; } TEST(skill_transfer_has_hints) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - skill->setup(json::object({ - {"transfers", json::object({ - {"sales-team", json::object({{"url", "x"}})} - })} - })); - auto hints = skill->get_hints(); - ASSERT_TRUE(hints.size() >= 1u); - // Should contain at least "transfer" or "connect" - bool has_any = false; - for (const auto& h : hints) { - if (h == "transfer" || h == "connect") has_any = true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->setup( + json::object({{"transfers", json::object({{"sales-team", json::object({{"url", "x"}})}})}}))); + auto hints = skill->get_hints(); + ASSERT_TRUE(!hints.empty()); + // Should contain at least "transfer" or "connect" + bool has_any = false; + for (const auto& h : hints) { + if (h == "transfer" || h == "connect") { + has_any = true; } - ASSERT_TRUE(has_any); - return true; + } + ASSERT_TRUE(has_any); + return true; } TEST(skill_transfer_no_webhook_tools) { - auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); - skill->setup(json::object({ - {"transfers", json::object({{"sales", json::object({{"url", "x"}})}})} - })); - ASSERT_EQ(skill->register_tools().size(), 0u); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("swml_transfer"); + ASSERT_TRUE(skill->setup( + json::object({{"transfers", json::object({{"sales", json::object({{"url", "x"}})}})}}))); + ASSERT_EQ(skill->register_tools().size(), 0u); + return true; } diff --git a/tests/test_skill_trivia.cpp b/tests/test_skill_trivia.cpp index 9f0069b..2123b13 100644 --- a/tests/test_skill_trivia.cpp +++ b/tests/test_skill_trivia.cpp @@ -4,53 +4,53 @@ namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_trivia_name) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - ASSERT_EQ(skill->skill_name(), "api_ninjas_trivia"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_EQ(skill->skill_name(), "api_ninjas_trivia"); + return true; } TEST(skill_trivia_multi_instance) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - ASSERT_TRUE(skill->supports_multiple_instances()); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->supports_multiple_instances()); + return true; } TEST(skill_trivia_setup_with_key) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - ASSERT_TRUE(skill->setup(json::object({{"api_key", "test-key"}}))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "test-key"}}))); + return true; } TEST(skill_trivia_returns_datamap) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - skill->setup(json::object({{"api_key", "key"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_EQ(dm.size(), 1u); - ASSERT_EQ(dm[0]["function"].get(), "get_trivia"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "key"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_EQ(dm.size(), 1u); + ASSERT_EQ(dm[0]["function"].get(), "get_trivia"); + return true; } TEST(skill_trivia_datamap_has_webhook) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - skill->setup(json::object({{"api_key", "key"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_TRUE(dm[0]["data_map"].contains("webhooks")); - auto url = dm[0]["data_map"]["webhooks"][0]["url"].get(); - ASSERT_TRUE(url.find("api-ninjas.com") != std::string::npos); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "key"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_TRUE(dm[0]["data_map"].contains("webhooks")); + auto url = dm[0]["data_map"]["webhooks"][0]["url"].get(); + ASSERT_TRUE(url.find("api-ninjas.com") != std::string::npos); + return true; } TEST(skill_trivia_has_category_param) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - skill->setup(json::object({{"api_key", "key"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_TRUE(dm[0]["parameters"]["properties"].contains("category")); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "key"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_TRUE(dm[0]["parameters"]["properties"].contains("category")); + return true; } TEST(skill_trivia_no_webhook_tools) { - auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); - skill->setup(json::object({{"api_key", "key"}})); - ASSERT_EQ(skill->register_tools().size(), 0u); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("api_ninjas_trivia"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "key"}}))); + ASSERT_EQ(skill->register_tools().size(), 0u); + return true; } diff --git a/tests/test_skill_vector_search.cpp b/tests/test_skill_vector_search.cpp index 4e9a276..87a055c 100644 --- a/tests/test_skill_vector_search.cpp +++ b/tests/test_skill_vector_search.cpp @@ -4,85 +4,85 @@ namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_vectorsearch_name) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - ASSERT_EQ(skill->skill_name(), "native_vector_search"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_EQ(skill->skill_name(), "native_vector_search"); + return true; } TEST(skill_vectorsearch_multi_instance) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - ASSERT_TRUE(skill->supports_multiple_instances()); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->supports_multiple_instances()); + return true; } TEST(skill_vectorsearch_setup_requires_source) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - ASSERT_FALSE(skill->setup(json::object())); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_FALSE(skill->setup(json::object())); + return true; } TEST(skill_vectorsearch_setup_with_remote) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - ASSERT_TRUE(skill->setup(json::object({{"remote_url", "https://search.example.com"}}))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "https://search.example.com"}}))); + return true; } TEST(skill_vectorsearch_setup_with_index_file) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - ASSERT_TRUE(skill->setup(json::object({{"index_file", "/path/to/index.swsearch"}}))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"index_file", "/path/to/index.swsearch"}}))); + return true; } TEST(skill_vectorsearch_registers_tool) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - skill->setup(json::object({{"remote_url", "https://search.example.com"}})); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); - ASSERT_EQ(tools[0].name, "search_knowledge"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "https://search.example.com"}}))); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); + ASSERT_EQ(tools[0].name, "search_knowledge"); + return true; } TEST(skill_vectorsearch_custom_tool_name) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - skill->setup(json::object({{"remote_url", "x"}, {"tool_name", "search_docs"}})); - auto tools = skill->register_tools(); - ASSERT_EQ(tools[0].name, "search_docs"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "x"}, {"tool_name", "search_docs"}}))); + auto tools = skill->register_tools(); + ASSERT_EQ(tools[0].name, "search_docs"); + return true; } TEST(skill_vectorsearch_handler_empty_query) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - skill->setup(json::object({{"remote_url", "https://search.example.com"}})); - auto tools = skill->register_tools(); - // Empty query -> the prompt-for-query message (mirrors Python). - auto result = tools[0].handler(json::object({{"query", ""}}), json::object()); - auto resp = result.to_json()["response"].get(); - ASSERT_TRUE(resp.find("search query") != std::string::npos); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "https://search.example.com"}}))); + auto tools = skill->register_tools(); + // Empty query -> the prompt-for-query message (mirrors Python). + auto result = tools[0].handler(json::object({{"query", ""}}), json::object()); + auto resp = result.to_json()["response"].get(); + ASSERT_TRUE(resp.find("search query") != std::string::npos); + return true; } TEST(skill_vectorsearch_handler_remote_unreachable_reports_error) { - // In network mode the handler makes a REAL POST. Point it at an - // unroutable host (RFC 5737 TEST-NET-1) so the transport fails fast and the - // handler surfaces a real error — NOT a "[Would query…]" stub string. The - // live-POST happy path is covered in test_tier2_behavioral.cpp against a - // mock server. - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - skill->setup(json::object({{"remote_url", "http://192.0.2.1:9"}})); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", "test"}}), json::object()); - auto resp = result.to_json()["response"].get(); - ASSERT_TRUE(resp.find("Would query") == std::string::npos); - ASSERT_TRUE(resp.find("Remote search error") != std::string::npos); - return true; + // In network mode the handler makes a REAL POST. Point it at an + // unroutable host (RFC 5737 TEST-NET-1) so the transport fails fast and the + // handler surfaces a real error — NOT a "[Would query…]" stub string. The + // live-POST happy path is covered in test_tier2_behavioral.cpp against a + // mock server. + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "http://192.0.2.1:9"}}))); + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", "test"}}), json::object()); + auto resp = result.to_json()["response"].get(); + ASSERT_TRUE(resp.find("Would query") == std::string::npos); + ASSERT_TRUE(resp.find("Remote search error") != std::string::npos); + return true; } TEST(skill_vectorsearch_get_hints) { - auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); - skill->setup(json::object({{"remote_url", "x"}})); - auto hints = skill->get_hints(); - // May return empty or some hints depending on implementation - // Just verify it doesn't crash and returns a vector - (void)hints; - return true; + auto skill = sw_skills::SkillRegistry::instance().create("native_vector_search"); + ASSERT_TRUE(skill->setup(json::object({{"remote_url", "x"}}))); + auto hints = skill->get_hints(); + // May return empty or some hints depending on implementation + // Just verify it doesn't crash and returns a vector + (void)hints; + return true; } diff --git a/tests/test_skill_weather.cpp b/tests/test_skill_weather.cpp index 4d262b8..f8ccd3c 100644 --- a/tests/test_skill_weather.cpp +++ b/tests/test_skill_weather.cpp @@ -4,56 +4,56 @@ namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_weather_name) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - ASSERT_EQ(skill->skill_name(), "weather_api"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_EQ(skill->skill_name(), "weather_api"); + return true; } TEST(skill_weather_setup_with_key) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - ASSERT_TRUE(skill->setup(json::object({{"api_key", "test-key"}}))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "test-key"}}))); + return true; } TEST(skill_weather_returns_datamap) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - skill->setup(json::object({{"api_key", "test-key"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_EQ(dm.size(), 1u); - ASSERT_EQ(dm[0]["function"].get(), "get_weather"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "test-key"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_EQ(dm.size(), 1u); + ASSERT_EQ(dm[0]["function"].get(), "get_weather"); + return true; } TEST(skill_weather_custom_tool_name) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - skill->setup(json::object({{"api_key", "k"}, {"tool_name", "check_weather"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_EQ(dm[0]["function"].get(), "check_weather"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"tool_name", "check_weather"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_EQ(dm[0]["function"].get(), "check_weather"); + return true; } TEST(skill_weather_datamap_has_webhook) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - skill->setup(json::object({{"api_key", "k"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_TRUE(dm[0]["data_map"].contains("webhooks")); - ASSERT_EQ(dm[0]["data_map"]["webhooks"].size(), 1u); - auto url = dm[0]["data_map"]["webhooks"][0]["url"].get(); - ASSERT_TRUE(url.find("weatherapi.com") != std::string::npos); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_TRUE(dm[0]["data_map"].contains("webhooks")); + ASSERT_EQ(dm[0]["data_map"]["webhooks"].size(), 1u); + auto url = dm[0]["data_map"]["webhooks"][0]["url"].get(); + ASSERT_TRUE(url.find("weatherapi.com") != std::string::npos); + return true; } TEST(skill_weather_has_location_param) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - skill->setup(json::object({{"api_key", "k"}})); - auto dm = skill->get_datamap_functions(); - ASSERT_TRUE(dm[0]["parameters"]["properties"].contains("location")); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}}))); + auto dm = skill->get_datamap_functions(); + ASSERT_TRUE(dm[0]["parameters"]["properties"].contains("location")); + return true; } TEST(skill_weather_no_webhook_tools) { - auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); - skill->setup(json::object({{"api_key", "k"}})); - ASSERT_EQ(skill->register_tools().size(), 0u); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("weather_api"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}}))); + ASSERT_EQ(skill->register_tools().size(), 0u); + return true; } diff --git a/tests/test_skill_websearch.cpp b/tests/test_skill_websearch.cpp index eef7858..fae64b2 100644 --- a/tests/test_skill_websearch.cpp +++ b/tests/test_skill_websearch.cpp @@ -1,53 +1,53 @@ // Web search skill tests -#include "signalwire/skills/skill_registry.hpp" -#include "httplib.h" #include #include #include + +#include "httplib.h" +#include "signalwire/skills/skill_registry.hpp" namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_websearch_name) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - ASSERT_EQ(skill->skill_name(), "web_search"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_EQ(skill->skill_name(), "web_search"); + return true; } TEST(skill_websearch_multi_instance) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - ASSERT_TRUE(skill->supports_multiple_instances()); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->supports_multiple_instances()); + return true; } TEST(skill_websearch_version) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - ASSERT_EQ(skill->skill_version(), "2.0.0"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_EQ(skill->skill_version(), "2.0.0"); + return true; } TEST(skill_websearch_setup_with_keys) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - ASSERT_TRUE(skill->setup(json::object({ - {"api_key", "gkey"}, {"search_engine_id", "seid"} - }))); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "gkey"}, {"search_engine_id", "seid"}}))); + return true; } TEST(skill_websearch_registers_tool) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); - ASSERT_EQ(tools[0].name, "web_search"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); + ASSERT_EQ(tools[0].name, "web_search"); + return true; } TEST(skill_websearch_custom_tool_name) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}, {"tool_name", "search"}})); - auto tools = skill->register_tools(); - ASSERT_EQ(tools[0].name, "search"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup( + json::object({{"api_key", "k"}, {"search_engine_id", "s"}, {"tool_name", "search"}}))); + auto tools = skill->register_tools(); + ASSERT_EQ(tools[0].name, "search"); + return true; } // Drive the handler against a local HTTP fixture: prove that the skill @@ -56,40 +56,44 @@ TEST(skill_websearch_custom_tool_name) { // kernel-assigned ephemeral port and answers the customsearch path with // a minimal Google CSE-shaped body. TEST(skill_websearch_handler_works) { - httplib::Server srv; - std::atomic got_request{false}; - std::string captured_path; - srv.Get("/customsearch/v1", [&](const httplib::Request& req, httplib::Response& res) { - got_request = true; - captured_path = req.path + (req.params.empty() ? "" : "?..."); - res.set_content(R"({"items":[{"title":"Test Result","link":"https://t/1","snippet":"hit for test query"}]})", - "application/json"); - }); - - int port = 0; - std::thread th([&]{ port = srv.bind_to_any_port("127.0.0.1"); srv.listen_after_bind(); }); - // Spin until bound. - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - ASSERT_TRUE(port > 0); - - ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", "test query"}}), json::object()); - auto resp = result.to_json()["response"].get(); - - srv.stop(); - th.join(); - ::unsetenv("WEB_SEARCH_BASE_URL"); - - ASSERT_TRUE(got_request); // proves the skill issued real HTTP - ASSERT_TRUE(resp.find("test query") != std::string::npos); - ASSERT_TRUE(resp.find("Test Result") != std::string::npos); // proves real parse - return true; + httplib::Server srv; + std::atomic got_request{false}; + std::string captured_path; + srv.Get("/customsearch/v1", [&](const httplib::Request& req, httplib::Response& res) { + got_request = true; + captured_path = req.path + (req.params.empty() ? "" : "?..."); + res.set_content( + R"({"items":[{"title":"Test Result","link":"https://t/1","snippet":"hit for test query"}]})", + "application/json"); + }); + + int port = 0; + std::thread th([&] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + // Spin until bound. + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ASSERT_TRUE(port > 0); + + ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", "test query"}}), json::object()); + auto resp = result.to_json()["response"].get(); + + srv.stop(); + th.join(); + ::unsetenv("WEB_SEARCH_BASE_URL"); + + ASSERT_TRUE(got_request); // proves the skill issued real HTTP + ASSERT_TRUE(resp.find("test query") != std::string::npos); + ASSERT_TRUE(resp.find("Test Result") != std::string::npos); // proves real parse + return true; } // ============================================================================ @@ -103,81 +107,88 @@ namespace { // skill at it, call the handler, return the response string. Cleans up // the server + env var on the way out so test ordering doesn't matter. static std::string run_websearch_with_params(const json& extra_params, - const std::string& query = "test query") { - httplib::Server srv; - srv.Get("/customsearch/v1", [&](const httplib::Request&, httplib::Response& res) { - res.set_content(R"({"items":[{"title":"Test Result","link":"https://t/1","snippet":"hit for test query"}]})", - "application/json"); - }); - int port = 0; - std::thread th([&]{ port = srv.bind_to_any_port("127.0.0.1"); srv.listen_after_bind(); }); - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); - - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - json setup_params = json::object({{"api_key", "k"}, {"search_engine_id", "s"}}); - for (auto& [k, v] : extra_params.items()) setup_params[k] = v; - skill->setup(setup_params); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", query}}), json::object()); - std::string resp = result.to_json()["response"].get(); - - srv.stop(); - th.join(); - ::unsetenv("WEB_SEARCH_BASE_URL"); - return resp; + const std::string& query = "test query") { + httplib::Server srv; + srv.Get("/customsearch/v1", [&](const httplib::Request&, httplib::Response& res) { + res.set_content( + R"({"items":[{"title":"Test Result","link":"https://t/1","snippet":"hit for test query"}]})", + "application/json"); + }); + int port = 0; + std::thread th([&] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + json setup_params = json::object({{"api_key", "k"}, {"search_engine_id", "s"}}); + for (auto& [k, v] : extra_params.items()) { + setup_params[k] = v; + } + if (!skill->setup(setup_params)) { + return ""; + } + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", query}}), json::object()); + std::string resp = result.to_json()["response"].get(); + + srv.stop(); + th.join(); + ::unsetenv("WEB_SEARCH_BASE_URL"); + return resp; } } // namespace TEST(skill_websearch_no_prefix_postfix_unchanged) { - // Baseline: without the params, the body has no wrapper text. - std::string resp = run_websearch_with_params(json::object()); - ASSERT_TRUE(resp.find("Test Result") != std::string::npos); - ASSERT_TRUE(resp.find("AGENT_HINT") == std::string::npos); - ASSERT_TRUE(resp.find("SOURCE_NOTE") == std::string::npos); - return true; + // Baseline: without the params, the body has no wrapper text. + std::string resp = run_websearch_with_params(json::object()); + ASSERT_TRUE(resp.find("Test Result") != std::string::npos); + ASSERT_TRUE(resp.find("AGENT_HINT") == std::string::npos); + ASSERT_TRUE(resp.find("SOURCE_NOTE") == std::string::npos); + return true; } TEST(skill_websearch_response_prefix_wraps_success) { - std::string resp = run_websearch_with_params( - json::object({{"response_prefix", "AGENT_HINT: from-public-web"}})); - // Prefix appears, response body appears, and prefix precedes the body. - auto p = resp.find("AGENT_HINT: from-public-web"); - auto b = resp.find("Test Result"); - ASSERT_TRUE(p != std::string::npos); - ASSERT_TRUE(b != std::string::npos); - ASSERT_TRUE(p < b); - return true; + std::string resp = + run_websearch_with_params(json::object({{"response_prefix", "AGENT_HINT: from-public-web"}})); + // Prefix appears, response body appears, and prefix precedes the body. + auto p = resp.find("AGENT_HINT: from-public-web"); + auto b = resp.find("Test Result"); + ASSERT_TRUE(p != std::string::npos); + ASSERT_TRUE(b != std::string::npos); + ASSERT_TRUE(p < b); + return true; } TEST(skill_websearch_response_postfix_wraps_success) { - std::string resp = run_websearch_with_params( - json::object({{"response_postfix", "SOURCE_NOTE: public-search"}})); - auto b = resp.find("Test Result"); - auto pf = resp.find("SOURCE_NOTE: public-search"); - ASSERT_TRUE(b != std::string::npos); - ASSERT_TRUE(pf != std::string::npos); - ASSERT_TRUE(b < pf); - return true; + std::string resp = + run_websearch_with_params(json::object({{"response_postfix", "SOURCE_NOTE: public-search"}})); + auto b = resp.find("Test Result"); + auto pf = resp.find("SOURCE_NOTE: public-search"); + ASSERT_TRUE(b != std::string::npos); + ASSERT_TRUE(pf != std::string::npos); + ASSERT_TRUE(b < pf); + return true; } TEST(skill_websearch_response_prefix_and_postfix_both_wrap) { - std::string resp = run_websearch_with_params(json::object({ - {"response_prefix", "AGENT_HINT: from-public-web"}, - {"response_postfix", "SOURCE_NOTE: public-search"} - })); - auto p = resp.find("AGENT_HINT: from-public-web"); - auto b = resp.find("Test Result"); - auto pf = resp.find("SOURCE_NOTE: public-search"); - ASSERT_TRUE(p != std::string::npos); - ASSERT_TRUE(b != std::string::npos); - ASSERT_TRUE(pf != std::string::npos); - ASSERT_TRUE(p < b); - ASSERT_TRUE(b < pf); - return true; + std::string resp = + run_websearch_with_params(json::object({{"response_prefix", "AGENT_HINT: from-public-web"}, + {"response_postfix", "SOURCE_NOTE: public-search"}})); + auto p = resp.find("AGENT_HINT: from-public-web"); + auto b = resp.find("Test Result"); + auto pf = resp.find("SOURCE_NOTE: public-search"); + ASSERT_TRUE(p != std::string::npos); + ASSERT_TRUE(b != std::string::npos); + ASSERT_TRUE(pf != std::string::npos); + ASSERT_TRUE(p < b); + ASSERT_TRUE(b < pf); + return true; } // SECURITY (r5 F3.3): a web_search failure must NOT leak the Google CSE api_key @@ -187,51 +198,50 @@ TEST(skill_websearch_response_prefix_and_postfix_both_wrap) { // Drive a real transport failure by pointing the base URL at a closed port, then // assert the response contains neither the key nor `key=`. TEST(skill_websearch_transport_error_redacts_api_key) { - // Bind an ephemeral port, capture it, then release it so the connect refuses. - int dead_port = 0; - { - httplib::Server probe; - std::thread pth([&]{ dead_port = probe.bind_to_any_port("127.0.0.1"); }); - auto dl = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (dead_port == 0 && std::chrono::steady_clock::now() < dl) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - probe.stop(); - pth.join(); + // Bind an ephemeral port, capture it, then release it so the connect refuses. + int dead_port = 0; + { + httplib::Server probe; + std::thread pth([&] { dead_port = probe.bind_to_any_port("127.0.0.1"); }); + auto dl = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (dead_port == 0 && std::chrono::steady_clock::now() < dl) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - ASSERT_TRUE(dead_port > 0); - - const std::string secret = "AIzaSyLEAKTESTSECRETKEY0123456789"; - ::setenv("WEB_SEARCH_BASE_URL", - ("http://127.0.0.1:" + std::to_string(dead_port)).c_str(), 1); - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", secret}, {"search_engine_id", "seid"}})); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", "test query"}}), json::object()); - std::string resp = result.to_json()["response"].get(); - ::unsetenv("WEB_SEARCH_BASE_URL"); - - // The key must never appear, and neither must the `key=` query param that - // would carry it (redaction strips the whole query). - ASSERT_TRUE(resp.find(secret) == std::string::npos); - ASSERT_TRUE(resp.find("key=") == std::string::npos); - return true; + probe.stop(); + pth.join(); + } + ASSERT_TRUE(dead_port > 0); + + const std::string secret = "AIzaSyLEAKTESTSECRETKEY0123456789"; + ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(dead_port)).c_str(), 1); + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", secret}, {"search_engine_id", "seid"}}))); + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", "test query"}}), json::object()); + std::string resp = result.to_json()["response"].get(); + ::unsetenv("WEB_SEARCH_BASE_URL"); + + // The key must never appear, and neither must the `key=` query param that + // would carry it (redaction strips the whole query). + ASSERT_TRUE(resp.find(secret) == std::string::npos); + ASSERT_TRUE(resp.find("key=") == std::string::npos); + return true; } TEST(skill_websearch_has_prompt_sections) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto sections = skill->get_prompt_sections(); - ASSERT_TRUE(sections.size() >= 1u); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto sections = skill->get_prompt_sections(); + ASSERT_TRUE(!sections.empty()); + return true; } TEST(skill_websearch_global_data) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto gd = skill->get_global_data(); - ASSERT_TRUE(gd.contains("web_search_enabled")); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto gd = skill->get_global_data(); + ASSERT_TRUE(gd.contains("web_search_enabled")); + return true; } // ============================================================================ @@ -255,79 +265,81 @@ namespace { // content fetch is mid-sleep. `hits` counts how many content fetches started, // so a test can assert whether scraping was attempted. struct LatencyFixture { - httplib::Server srv; - std::atomic hits{0}; - std::atomic stopping{false}; - std::thread th; - int port = 0; - - LatencyFixture(int num_items, int content_delay_ms) { - srv.Get("/customsearch/v1", [this, num_items](const httplib::Request&, - httplib::Response& res) { - json items = json::array(); - for (int i = 0; i < num_items; ++i) { - items.push_back(json::object({ - {"title", "Result " + std::to_string(i)}, - {"link", "http://127.0.0.1:" + std::to_string(port) + - "/page" + std::to_string(i)}, - {"snippet", "snippet " + std::to_string(i)} - })); - } - res.set_content(json::object({{"items", items}}).dump(), - "application/json"); - }); - // Catch-all content endpoint: record the hit, then sleep in 25ms slices - // up to content_delay_ms (or until teardown), then return rich HTML. - srv.Get(R"(/page\d+)", [this, content_delay_ms](const httplib::Request&, - httplib::Response& res) { - hits.fetch_add(1); - int slept = 0; - while (slept < content_delay_ms && !stopping.load()) { + httplib::Server srv; + std::atomic hits{0}; + std::atomic stopping{false}; + std::thread th; + int port = 0; + + LatencyFixture(int num_items, int content_delay_ms) { + srv.Get("/customsearch/v1", [this, num_items](const httplib::Request&, httplib::Response& res) { + json items = json::array(); + for (int i = 0; i < num_items; ++i) { + items.push_back(json::object( + {{"title", "Result " + std::to_string(i)}, + {"link", "http://127.0.0.1:" + std::to_string(port) + "/page" + std::to_string(i)}, + {"snippet", "snippet " + std::to_string(i)}})); + } + res.set_content(json::object({{"items", items}}).dump(), "application/json"); + }); + // Catch-all content endpoint: record the hit, then sleep in 25ms slices + // up to content_delay_ms (or until teardown), then return rich HTML. + srv.Get(R"(/page\d+)", + [this, content_delay_ms](const httplib::Request&, httplib::Response& res) { + hits.fetch_add(1); + int slept = 0; + while (slept < content_delay_ms && !stopping.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(25)); slept += 25; - } - res.set_content("
" - "Lots of relevant page content here. " - "Lots of relevant page content here.
", - "text/html"); - }); - th = std::thread([this] { - port = srv.bind_to_any_port("127.0.0.1"); - srv.listen_after_bind(); - }); - auto dl = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < dl) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - ::setenv("WEB_SEARCH_BASE_URL", - ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + } + res.set_content( + "
" + "Lots of relevant page content here. " + "Lots of relevant page content here.
", + "text/html"); + }); + th = std::thread([this] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto dl = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < dl) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } + ::setenv("WEB_SEARCH_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + } - ~LatencyFixture() { - stopping.store(true); - srv.stop(); - if (th.joinable()) th.join(); - ::unsetenv("WEB_SEARCH_BASE_URL"); + ~LatencyFixture() { + stopping.store(true); + srv.stop(); + if (th.joinable()) { + th.join(); } + ::unsetenv("WEB_SEARCH_BASE_URL"); + } }; // Run the registered web_search handler with `extra` setup params merged over a // fast/deterministic baseline. Returns {response, elapsed_ms}. static std::pair run_latency_handler(const json& extra, const std::string& query) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - json setup = json::object({ - {"api_key", "k"}, {"search_engine_id", "s"}, {"num_results", 2} - }); - for (auto& [k, v] : extra.items()) setup[k] = v; - skill->setup(setup); - auto tools = skill->register_tools(); - auto t0 = std::chrono::steady_clock::now(); - auto result = tools[0].handler(json::object({{"query", query}}), json::object()); - auto t1 = std::chrono::steady_clock::now(); - std::string resp = result.to_json()["response"].get(); - long ms = std::chrono::duration_cast(t1 - t0).count(); - return {resp, ms}; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + json setup = json::object({{"api_key", "k"}, {"search_engine_id", "s"}, {"num_results", 2}}); + for (auto& [k, v] : extra.items()) { + setup[k] = v; + } + if (!skill->setup(setup)) { + // Same failure-sentinel shape the other helpers use; a failed setup must + // not fall through into timing an unconfigured skill. + return {"", -1}; + } + auto tools = skill->register_tools(); + auto t0 = std::chrono::steady_clock::now(); + auto result = tools[0].handler(json::object({{"query", query}}), json::object()); + auto t1 = std::chrono::steady_clock::now(); + std::string resp = result.to_json()["response"].get(); + long ms = std::chrono::duration_cast(t1 - t0).count(); + return {resp, ms}; } } // namespace @@ -336,55 +348,53 @@ static std::pair run_latency_handler(const json& extra, // snippets_only=false are advertised in the schema with the matching type + // default. (Setup() reads them; the schema is the observable surface.) TEST(skill_websearch_latency_defaults_in_schema) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto schema = skill->get_parameter_schema(); - ASSERT_TRUE(schema.contains("per_page_timeout")); - ASSERT_EQ(schema["per_page_timeout"]["type"].get(), "number"); - ASSERT_EQ(schema["per_page_timeout"]["default"].get(), 2.0); - ASSERT_TRUE(schema.contains("overall_deadline")); - ASSERT_EQ(schema["overall_deadline"]["default"].get(), 10.0); - ASSERT_TRUE(schema.contains("parallel_scrape")); - ASSERT_EQ(schema["parallel_scrape"]["type"].get(), "boolean"); - ASSERT_EQ(schema["parallel_scrape"]["default"].get(), true); - ASSERT_TRUE(schema.contains("snippets_only")); - ASSERT_EQ(schema["snippets_only"]["default"].get(), false); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto schema = skill->get_parameter_schema(); + ASSERT_TRUE(schema.contains("per_page_timeout")); + ASSERT_EQ(schema["per_page_timeout"]["type"].get(), "number"); + ASSERT_EQ(schema["per_page_timeout"]["default"].get(), 2.0); + ASSERT_TRUE(schema.contains("overall_deadline")); + ASSERT_EQ(schema["overall_deadline"]["default"].get(), 10.0); + ASSERT_TRUE(schema.contains("parallel_scrape")); + ASSERT_EQ(schema["parallel_scrape"]["type"].get(), "boolean"); + ASSERT_EQ(schema["parallel_scrape"]["default"].get(), true); + ASSERT_TRUE(schema.contains("snippets_only")); + ASSERT_EQ(schema["snippets_only"]["default"].get(), false); + return true; } // Schema drift guard (Python parity: test_every_setup_param_is_advertised). // All 6 latency/response params must be advertised, each not required. TEST(skill_websearch_schema_advertises_all_six) { - auto skill = sw_skills::SkillRegistry::instance().create("web_search"); - skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}})); - auto schema = skill->get_parameter_schema(); - for (const char* key : {"response_prefix", "response_postfix", - "per_page_timeout", "overall_deadline", - "parallel_scrape", "snippets_only"}) { - ASSERT_TRUE(schema.contains(key)); - ASSERT_EQ(schema[key]["required"].get(), false); - } - ASSERT_EQ(schema["response_prefix"]["default"].get(), ""); - ASSERT_EQ(schema["response_postfix"]["default"].get(), ""); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("web_search"); + ASSERT_TRUE(skill->setup(json::object({{"api_key", "k"}, {"search_engine_id", "s"}}))); + auto schema = skill->get_parameter_schema(); + for (const char* key : {"response_prefix", "response_postfix", "per_page_timeout", + "overall_deadline", "parallel_scrape", "snippets_only"}) { + ASSERT_TRUE(schema.contains(key)); + ASSERT_EQ(schema[key]["required"].get(), false); + } + ASSERT_EQ(schema["response_prefix"]["default"].get(), ""); + ASSERT_EQ(schema["response_postfix"]["default"].get(), ""); + return true; } // snippets_only must short-circuit BEFORE any page fetch. The content endpoint // would sleep 5s; if scraping ran the call would take ~5s. Instead it returns // immediately with snippet-only formatting and ZERO content hits. TEST(skill_websearch_snippets_only_skips_scraping) { - LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler( - json::object({{"snippets_only", true}}), "golang"); - - ASSERT_EQ(fx.hits.load(), 0); // proves no page was fetched - ASSERT_TRUE(ms < 2000); // sub-second-ish, not the 5s stall - ASSERT_TRUE(resp.find("Snippet-only results for 'golang'") != std::string::npos); - ASSERT_TRUE(resp.find("snippet 0") != std::string::npos); // snippet carried - ASSERT_TRUE(resp.find("page content not scraped") != std::string::npos); - return true; + LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler(json::object({{"snippets_only", true}}), "golang"); + + ASSERT_EQ(fx.hits.load(), 0); // proves no page was fetched + ASSERT_TRUE(ms < 2000); // sub-second-ish, not the 5s stall + ASSERT_TRUE(resp.find("Snippet-only results for 'golang'") != std::string::npos); + ASSERT_TRUE(resp.find("snippet 0") != std::string::npos); // snippet carried + ASSERT_TRUE(resp.find("page content not scraped") != std::string::npos); + return true; } // overall_deadline IS THE CONTRACT: a content server that stalls 5s with a 1s @@ -392,42 +402,42 @@ TEST(skill_websearch_snippets_only_skips_scraping) { // back to the CSE snippets — never an empty no-results message. Parallel mode. // per_page_timeout is set large so the DEADLINE (not per-page) is what truncates. TEST(skill_websearch_overall_deadline_truncates_to_snippet_fallback) { - LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/5000); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler(json::object({ - {"overall_deadline", 1.0}, // budget well under the 5s content stall - {"per_page_timeout", 30.0}, // large, so the deadline truncates - {"parallel_scrape", true} - }), "kubernetes"); - - // Returned at the deadline, not after the full 5s stall (allow slack). - ASSERT_TRUE(ms < 3000); - // Non-empty snippet fallback, NOT the empty no-results / no-items message. - ASSERT_TRUE(resp.find("Snippet-only results for 'kubernetes'") != std::string::npos); - ASSERT_TRUE(resp.find("(no results)") == std::string::npos); - ASSERT_TRUE(resp.find("snippet 0") != std::string::npos); - ASSERT_FALSE(resp.empty()); - return true; + LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/5000); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler( + json::object({{"overall_deadline", 1.0}, // budget well under the 5s content stall + {"per_page_timeout", 30.0}, // large, so the deadline truncates + {"parallel_scrape", true}}), + "kubernetes"); + + // Returned at the deadline, not after the full 5s stall (allow slack). + ASSERT_TRUE(ms < 3000); + // Non-empty snippet fallback, NOT the empty no-results / no-items message. + ASSERT_TRUE(resp.find("Snippet-only results for 'kubernetes'") != std::string::npos); + ASSERT_TRUE(resp.find("(no results)") == std::string::npos); + ASSERT_TRUE(resp.find("snippet 0") != std::string::npos); + ASSERT_FALSE(resp.empty()); + return true; } // Same contract in sequential mode (parallel_scrape=false). With a 5s-per-page // stall and a 1s budget, per_page_timeout=0.5s force-fails each fetch; the loop // then sees the (already-)passed deadline and breaks, falling back to snippets. TEST(skill_websearch_overall_deadline_sequential_falls_back) { - LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/5000); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler(json::object({ - {"overall_deadline", 1.0}, - {"per_page_timeout", 0.5}, // a single fetch is force-failed at 0.5s - {"parallel_scrape", false} - }), "rustlang"); - - ASSERT_TRUE(ms < 3000); - ASSERT_TRUE(resp.find("Snippet-only results for 'rustlang'") != std::string::npos); - ASSERT_TRUE(resp.find("(no results)") == std::string::npos); - return true; + LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/5000); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler( + json::object({{"overall_deadline", 1.0}, + {"per_page_timeout", 0.5}, // a single fetch is force-failed at 0.5s + {"parallel_scrape", false}}), + "rustlang"); + + ASSERT_TRUE(ms < 3000); + ASSERT_TRUE(resp.find("Snippet-only results for 'rustlang'") != std::string::npos); + ASSERT_TRUE(resp.find("(no results)") == std::string::npos); + return true; } // per_page_timeout caps a single fetch independent of the overall budget. With @@ -435,19 +445,19 @@ TEST(skill_websearch_overall_deadline_sequential_falls_back) { // parallel mode, every fetch errors near 0.3s, no page yields content, and we // fall back to snippets WELL before the 5s stall (and before the 8s budget). TEST(skill_websearch_per_page_timeout_honored) { - LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler(json::object({ - {"per_page_timeout", 0.3}, - {"overall_deadline", 8.0}, // large, so it can't be what truncates - {"parallel_scrape", true} - }), "elixir"); - - ASSERT_TRUE(ms < 3000); // governed by 0.3s per-page, not 5s/8s - ASSERT_TRUE(fx.hits.load() > 0); // fetches were attempted (then timed out) - ASSERT_TRUE(resp.find("Snippet-only results for 'elixir'") != std::string::npos); - return true; + LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler( + json::object({{"per_page_timeout", 0.3}, + {"overall_deadline", 8.0}, // large, so it can't be what truncates + {"parallel_scrape", true}}), + "elixir"); + + ASSERT_TRUE(ms < 3000); // governed by 0.3s per-page, not 5s/8s + ASSERT_TRUE(fx.hits.load() > 0); // fetches were attempted (then timed out) + ASSERT_TRUE(resp.find("Snippet-only results for 'elixir'") != std::string::npos); + return true; } // Happy path UNDER the deadline: a FAST content server (no stall) with parallel @@ -455,46 +465,44 @@ TEST(skill_websearch_per_page_timeout_honored) { // fallback — proving the std::async harvest path delivers real content when // pages are quick. TEST(skill_websearch_parallel_fast_path_scrapes_content) { - LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/0); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler(json::object({ - {"overall_deadline", 10.0}, - {"per_page_timeout", 5.0}, - {"parallel_scrape", true} - }), "postgres"); - - ASSERT_TRUE(fx.hits.load() > 0); // pages were actually fetched - // Fully-scraped output includes the page-content header + scraped marker. - ASSERT_TRUE(resp.find("page content scraped") != std::string::npos); - ASSERT_TRUE(resp.find("Content:") != std::string::npos); - ASSERT_TRUE(resp.find("Snippet-only results") == std::string::npos); - return true; + LatencyFixture fx(/*num_items=*/3, /*content_delay_ms=*/0); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler( + json::object( + {{"overall_deadline", 10.0}, {"per_page_timeout", 5.0}, {"parallel_scrape", true}}), + "postgres"); + + ASSERT_TRUE(fx.hits.load() > 0); // pages were actually fetched + // Fully-scraped output includes the page-content header + scraped marker. + ASSERT_TRUE(resp.find("page content scraped") != std::string::npos); + ASSERT_TRUE(resp.find("Content:") != std::string::npos); + ASSERT_TRUE(resp.find("Snippet-only results") == std::string::npos); + return true; } // Deadline fallback must still honor response_prefix/response_postfix wrapping // (the snippet fallback is a "non-empty success" path, unlike the no-items // branch). Proves wrapping composes with the latency machinery. TEST(skill_websearch_snippet_fallback_is_wrapped) { - LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); - ASSERT_TRUE(fx.port > 0); - - auto [resp, ms] = run_latency_handler(json::object({ - {"overall_deadline", 1.0}, - {"per_page_timeout", 0.4}, - {"parallel_scrape", true}, - {"response_prefix", "WRAP_PRE"}, - {"response_postfix", "WRAP_POST"} - }), "scala"); - - ASSERT_TRUE(ms < 3000); - auto pre = resp.find("WRAP_PRE"); - auto body = resp.find("Snippet-only results for 'scala'"); - auto post = resp.find("WRAP_POST"); - ASSERT_TRUE(pre != std::string::npos); - ASSERT_TRUE(body != std::string::npos); - ASSERT_TRUE(post != std::string::npos); - ASSERT_TRUE(pre < body); - ASSERT_TRUE(body < post); - return true; + LatencyFixture fx(/*num_items=*/2, /*content_delay_ms=*/5000); + ASSERT_TRUE(fx.port > 0); + + auto [resp, ms] = run_latency_handler(json::object({{"overall_deadline", 1.0}, + {"per_page_timeout", 0.4}, + {"parallel_scrape", true}, + {"response_prefix", "WRAP_PRE"}, + {"response_postfix", "WRAP_POST"}}), + "scala"); + + ASSERT_TRUE(ms < 3000); + auto pre = resp.find("WRAP_PRE"); + auto body = resp.find("Snippet-only results for 'scala'"); + auto post = resp.find("WRAP_POST"); + ASSERT_TRUE(pre != std::string::npos); + ASSERT_TRUE(body != std::string::npos); + ASSERT_TRUE(post != std::string::npos); + ASSERT_TRUE(pre < body); + ASSERT_TRUE(body < post); + return true; } diff --git a/tests/test_skill_wikipedia.cpp b/tests/test_skill_wikipedia.cpp index a90066d..ced8372 100644 --- a/tests/test_skill_wikipedia.cpp +++ b/tests/test_skill_wikipedia.cpp @@ -1,86 +1,91 @@ // Wikipedia search skill tests -#include "signalwire/skills/skill_registry.hpp" -#include "httplib.h" #include #include #include + +#include "httplib.h" +#include "signalwire/skills/skill_registry.hpp" namespace sw_skills = signalwire::skills; using json = nlohmann::json; TEST(skill_wikipedia_name) { - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - ASSERT_EQ(skill->skill_name(), "wikipedia_search"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_EQ(skill->skill_name(), "wikipedia_search"); + return true; } TEST(skill_wikipedia_setup_no_params) { - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - ASSERT_TRUE(skill->setup(json::object())); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_TRUE(skill->setup(json::object())); + return true; } TEST(skill_wikipedia_registers_tool) { - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - skill->setup(json::object()); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); - ASSERT_EQ(tools[0].name, "search_wiki"); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); + ASSERT_EQ(tools[0].name, "search_wiki"); + return true; } // Drive the handler against a local fixture so we prove the skill issues // real HTTP and parses the canned response, not just returns canned text. TEST(skill_wikipedia_handler_with_query) { - httplib::Server srv; - std::atomic got_request{false}; - srv.Get("/w/api.php", [&](const httplib::Request&, httplib::Response& res) { - got_request = true; - res.set_content(R"JSON({"query":{"search":[{"title":"Albert Einstein","snippet":"physicist (1879-1955)"}]}})JSON", - "application/json"); - }); + httplib::Server srv; + std::atomic got_request{false}; + srv.Get("/w/api.php", [&](const httplib::Request&, httplib::Response& res) { + got_request = true; + res.set_content( + R"JSON({"query":{"search":[{"title":"Albert Einstein","snippet":"physicist (1879-1955)"}]}})JSON", + "application/json"); + }); - int port = 0; - std::thread th([&]{ port = srv.bind_to_any_port("127.0.0.1"); srv.listen_after_bind(); }); - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - ASSERT_TRUE(port > 0); + int port = 0; + std::thread th([&] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ASSERT_TRUE(port > 0); - ::setenv("WIKIPEDIA_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - skill->setup(json::object()); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", "Albert Einstein"}}), json::object()); - auto resp = result.to_json()["response"].get(); + ::setenv("WIKIPEDIA_BASE_URL", ("http://127.0.0.1:" + std::to_string(port)).c_str(), 1); + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", "Albert Einstein"}}), json::object()); + auto resp = result.to_json()["response"].get(); - srv.stop(); - th.join(); - ::unsetenv("WIKIPEDIA_BASE_URL"); + srv.stop(); + th.join(); + ::unsetenv("WIKIPEDIA_BASE_URL"); - ASSERT_TRUE(got_request); - ASSERT_TRUE(resp.find("Albert Einstein") != std::string::npos); - ASSERT_TRUE(resp.find("physicist") != std::string::npos); // proves parse - return true; + ASSERT_TRUE(got_request); + ASSERT_TRUE(resp.find("Albert Einstein") != std::string::npos); + ASSERT_TRUE(resp.find("physicist") != std::string::npos); // proves parse + return true; } // Empty query short-circuits before any HTTP — confirm the no-results // message reaches the caller. The skill must NOT contact the upstream // for an empty query (no fixture needed). TEST(skill_wikipedia_empty_query_returns_response) { - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - skill->setup(json::object()); - auto tools = skill->register_tools(); - auto result = tools[0].handler(json::object({{"query", ""}}), json::object()); - auto resp = result.to_json()["response"].get(); - ASSERT_TRUE(resp.find("No Wikipedia") != std::string::npos); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + auto result = tools[0].handler(json::object({{"query", ""}}), json::object()); + auto resp = result.to_json()["response"].get(); + ASSERT_TRUE(resp.find("No Wikipedia") != std::string::npos); + return true; } TEST(skill_wikipedia_prompt_sections) { - auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); - skill->setup(json::object()); - auto sections = skill->get_prompt_sections(); - ASSERT_TRUE(sections.size() >= 1u); - return true; + auto skill = sw_skills::SkillRegistry::instance().create("wikipedia_search"); + ASSERT_TRUE(skill->setup(json::object())); + auto sections = skill->get_prompt_sections(); + ASSERT_TRUE(!sections.empty()); + return true; } diff --git a/tests/test_skills.cpp b/tests/test_skills.cpp index 7d471c6..26ea786 100644 --- a/tests/test_skills.cpp +++ b/tests/test_skills.cpp @@ -1,138 +1,139 @@ // Skills system tests #include "signalwire/skills/skill_base.hpp" -#include "signalwire/skills/skill_registry.hpp" #include "signalwire/skills/skill_manager.hpp" +#include "signalwire/skills/skill_registry.hpp" namespace sw_skills = signalwire::skills; using json = nlohmann::json; -// Force skill registration linkage -static bool _skills_init = (sw_skills::ensure_builtin_skills_registered(), true); +// Force skill registration linkage. Not `_skills_init`: a leading underscore at +// global scope is reserved for the implementation. +static bool skills_init = (sw_skills::ensure_builtin_skills_registered(), true); // ======================================================================== // Registry tests // ======================================================================== TEST(skill_registry_has_datetime) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("datetime")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("datetime")); + return true; } TEST(skill_registry_has_math) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("math")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("math")); + return true; } TEST(skill_registry_has_joke) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("joke")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("joke")); + return true; } TEST(skill_registry_has_weather_api) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("weather_api")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("weather_api")); + return true; } TEST(skill_registry_has_web_search) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("web_search")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("web_search")); + return true; } TEST(skill_registry_has_wikipedia_search) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("wikipedia_search")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("wikipedia_search")); + return true; } TEST(skill_registry_has_google_maps) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("google_maps")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("google_maps")); + return true; } TEST(skill_registry_has_spider) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("spider")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("spider")); + return true; } TEST(skill_registry_has_datasphere) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("datasphere")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("datasphere")); + return true; } TEST(skill_registry_has_datasphere_serverless) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("datasphere_serverless")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("datasphere_serverless")); + return true; } TEST(skill_registry_has_swml_transfer) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("swml_transfer")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("swml_transfer")); + return true; } TEST(skill_registry_has_play_background_file) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("play_background_file")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("play_background_file")); + return true; } TEST(skill_registry_has_api_ninjas_trivia) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("api_ninjas_trivia")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("api_ninjas_trivia")); + return true; } TEST(skill_registry_has_native_vector_search) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("native_vector_search")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("native_vector_search")); + return true; } TEST(skill_registry_has_info_gatherer) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("info_gatherer")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("info_gatherer")); + return true; } TEST(skill_registry_has_claude_skills) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("claude_skills")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("claude_skills")); + return true; } TEST(skill_registry_has_mcp_gateway) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("mcp_gateway")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("mcp_gateway")); + return true; } TEST(skill_registry_has_custom_skills) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_TRUE(reg.has_skill("custom_skills")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_TRUE(reg.has_skill("custom_skills")); + return true; } TEST(skill_registry_all_18_skills) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skills = reg.list_skills(); - ASSERT_TRUE(skills.size() >= 18u); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skills = reg.list_skills(); + ASSERT_TRUE(skills.size() >= 18u); + return true; } TEST(skill_registry_no_nonexistent) { - auto& reg = sw_skills::SkillRegistry::instance(); - ASSERT_FALSE(reg.has_skill("nonexistent_skill")); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + ASSERT_FALSE(reg.has_skill("nonexistent_skill")); + return true; } // ======================================================================== @@ -140,148 +141,174 @@ TEST(skill_registry_no_nonexistent) { // ======================================================================== TEST(skill_create_datetime) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("datetime"); - ASSERT_TRUE(skill != nullptr); - ASSERT_EQ(skill->skill_name(), "datetime"); - ASSERT_FALSE(skill->supports_multiple_instances()); - ASSERT_TRUE(skill->setup(json::object())); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("datetime"); + ASSERT_TRUE(skill != nullptr); + ASSERT_EQ(skill->skill_name(), "datetime"); + ASSERT_FALSE(skill->supports_multiple_instances()); + ASSERT_TRUE(skill->setup(json::object())); + return true; } TEST(skill_create_math) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("math"); - ASSERT_TRUE(skill != nullptr); - ASSERT_TRUE(skill->setup(json::object())); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); - ASSERT_EQ(tools[0].name, "calculate"); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("math"); + ASSERT_TRUE(skill != nullptr); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); + ASSERT_EQ(tools[0].name, "calculate"); + return true; } TEST(skill_math_calculate) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("math"); - skill->setup(json::object()); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("math"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); - auto result = tools[0].handler(json::object({{"expression", "2 + 3"}}), json::object()); - auto j = result.to_json(); - ASSERT_TRUE(j["response"].get().find("5") != std::string::npos); - return true; + auto result = tools[0].handler(json::object({{"expression", "2 + 3"}}), json::object()); + auto j = result.to_json(); + ASSERT_TRUE(j["response"].get().find("5") != std::string::npos); + return true; } TEST(skill_datetime_tools) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("datetime"); - skill->setup(json::object()); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 2u); - ASSERT_EQ(tools[0].name, "get_current_time"); - ASSERT_EQ(tools[1].name, "get_current_date"); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("datetime"); + ASSERT_TRUE(skill->setup(json::object())); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 2u); + ASSERT_EQ(tools[0].name, "get_current_time"); + ASSERT_EQ(tools[1].name, "get_current_date"); + return true; } TEST(skill_datetime_prompt_sections) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("datetime"); - skill->setup(json::object()); - auto sections = skill->get_prompt_sections(); - ASSERT_EQ(sections.size(), 1u); - ASSERT_EQ(sections[0].title, "Date and Time Information"); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("datetime"); + ASSERT_TRUE(skill->setup(json::object())); + auto sections = skill->get_prompt_sections(); + ASSERT_EQ(sections.size(), 1u); + ASSERT_EQ(sections[0].title, "Date and Time Information"); + return true; } TEST(skill_web_search_multi_instance) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("web_search"); - ASSERT_TRUE(skill->supports_multiple_instances()); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("web_search"); + ASSERT_TRUE(skill->supports_multiple_instances()); + return true; } TEST(skill_custom_skills_with_tools) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("custom_skills"); - json params = json::object({ - {"tools", json::array({ - json::object({ - {"name", "my_tool"}, - {"description", "My custom tool"}, - {"response", "Custom response"} - }) - })} - }); - ASSERT_TRUE(skill->setup(params)); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 1u); - ASSERT_EQ(tools[0].name, "my_tool"); - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("custom_skills"); + json params = + json::object({{"tools", json::array({json::object({{"name", "my_tool"}, + {"description", "My custom tool"}, + {"response", "Custom response"}})})}}); + ASSERT_TRUE(skill->setup(params)); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 1u); + ASSERT_EQ(tools[0].name, "my_tool"); + return true; } TEST(skill_info_gatherer_with_questions) { - auto& reg = sw_skills::SkillRegistry::instance(); - auto skill = reg.create("info_gatherer"); - json params = json::object({ - {"questions", json::array({ - json::object({{"key_name", "name"}, {"question_text", "What is your name?"}}), - json::object({{"key_name", "email"}, {"question_text", "What is your email?"}}) - })} - }); - ASSERT_TRUE(skill->setup(params)); - auto tools = skill->register_tools(); - ASSERT_EQ(tools.size(), 2u); // start_questions + submit_answer - return true; + auto& reg = sw_skills::SkillRegistry::instance(); + auto skill = reg.create("info_gatherer"); + json params = json::object( + {{"questions", + json::array( + {json::object({{"key_name", "name"}, {"question_text", "What is your name?"}}), + json::object({{"key_name", "email"}, {"question_text", "What is your email?"}})})}}); + ASSERT_TRUE(skill->setup(params)); + auto tools = skill->register_tools(); + ASSERT_EQ(tools.size(), 2u); // start_questions + submit_answer + return true; } // ======================================================================== // SkillManager // ======================================================================== +// load_skill's trailing two parameters are OPTIONAL (reference +// skill_manager.py:26) — these omit BOTH, so the defaults are what is +// exercised, not arguments the caller supplied. TEST(skill_manager_load) { - sw_skills::SkillManager mgr; - signalwire::agent::AgentBase agent; - bool loaded = mgr.load_skill("datetime", json::object(), agent); - ASSERT_TRUE(loaded); - ASSERT_TRUE(mgr.is_loaded("datetime")); - return true; + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + bool loaded = mgr.load_skill("datetime"); + ASSERT_TRUE(loaded); + ASSERT_TRUE(mgr.is_loaded("datetime")); + return true; } TEST(skill_manager_list_loaded) { - sw_skills::SkillManager mgr; - signalwire::agent::AgentBase agent; - (void)mgr.load_skill("datetime", json::object(), agent); - (void)mgr.load_skill("math", json::object(), agent); - auto loaded = mgr.list_loaded(); - ASSERT_EQ(loaded.size(), 2u); - return true; + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + (void)mgr.load_skill("datetime"); + (void)mgr.load_skill("math"); + auto loaded = mgr.list_loaded(); + ASSERT_EQ(loaded.size(), 2u); + return true; } TEST(skill_manager_unload) { - sw_skills::SkillManager mgr; - signalwire::agent::AgentBase agent; - (void)mgr.load_skill("datetime", json::object(), agent); - mgr.unload_skill("datetime"); - ASSERT_FALSE(mgr.is_loaded("datetime")); - return true; + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + (void)mgr.load_skill("datetime"); + mgr.unload_skill("datetime"); + ASSERT_FALSE(mgr.is_loaded("datetime")); + return true; } TEST(skill_manager_unknown_skill) { - sw_skills::SkillManager mgr; - signalwire::agent::AgentBase agent; - bool loaded = mgr.load_skill("nonexistent", json::object(), agent); - ASSERT_FALSE(loaded); - return true; + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + bool loaded = mgr.load_skill("nonexistent"); + ASSERT_FALSE(loaded); + return true; } TEST(skill_manager_no_duplicate_single_instance) { - sw_skills::SkillManager mgr; - signalwire::agent::AgentBase agent; - (void)mgr.load_skill("datetime", json::object(), agent); - bool second = mgr.load_skill("datetime", json::object(), agent); - ASSERT_FALSE(second); - return true; + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + (void)mgr.load_skill("datetime"); + bool second = mgr.load_skill("datetime"); + ASSERT_FALSE(second); + return true; +} + +// The reference's `skill_class` argument short-circuits the registry lookup. +// C++'s spelling is a SkillFactory. Passing one must bypass the name lookup +// entirely — proven by loading under a name the registry does NOT know. +TEST(skill_manager_explicit_skill_class_bypasses_registry) { + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + ASSERT_FALSE(sw_skills::SkillRegistry::instance().has_skill("not_in_registry")); + + sw_skills::SkillFactory factory = []() -> std::unique_ptr { + return sw_skills::SkillRegistry::instance().create("math"); + }; + bool loaded = mgr.load_skill("not_in_registry", factory); + ASSERT_TRUE(loaded); + ASSERT_TRUE(mgr.is_loaded("not_in_registry")); + return true; +} + +// params defaults to absent and must normalise to an empty object, not null. +TEST(skill_manager_params_default_is_empty_object) { + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + ASSERT_TRUE(mgr.load_skill("datetime")); + sw_skills::SkillBase* skill = mgr.get_skill("datetime"); + ASSERT_TRUE(skill != nullptr); + ASSERT_TRUE(skill->params().is_object()); + ASSERT_TRUE(skill->params().empty()); + return true; } // Reference parity: SkillBase.__init__(agent, params) stores `self.agent` and @@ -290,31 +317,28 @@ TEST(skill_manager_no_duplicate_single_instance) { // `setup(params)` and never held the agent at all, so a loaded skill could not // reach back to the agent that owns it. TEST(skill_manager_binds_agent_and_params_onto_skill) { - signalwire::agent::AgentBase agent; - sw_skills::SkillManager mgr(agent); - ASSERT_TRUE(mgr.agent() == &agent); + signalwire::agent::AgentBase agent; + sw_skills::SkillManager mgr(agent); + ASSERT_TRUE(mgr.agent() == &agent); - json params = json::object({{"prefix", "dt"}}); - bool loaded = mgr.load_skill("datetime", params, agent); - ASSERT_TRUE(loaded); + json params = json::object({{"prefix", "dt"}}); + bool loaded = mgr.load_skill("datetime", std::nullopt, params); + ASSERT_TRUE(loaded); - sw_skills::SkillBase* skill = mgr.get_skill("datetime"); - ASSERT_TRUE(skill != nullptr); - ASSERT_TRUE(skill->agent() == &agent); - ASSERT_EQ(skill->params()["prefix"], "dt"); - return true; + sw_skills::SkillBase* skill = mgr.get_skill("datetime"); + ASSERT_TRUE(skill != nullptr); + ASSERT_TRUE(skill->agent() == &agent); + ASSERT_EQ(skill->params()["prefix"], "dt"); + return true; } -// A default-constructed manager has no bound agent; the skill still gets the -// agent from the load_skill call. +// A default-constructed manager has no bound agent. The reference's manager is +// always agent-bound (SkillManager.__init__(agent)), so load_skill must fail +// LOUD here rather than silently loading into nothing. TEST(skill_manager_default_has_no_bound_agent) { - sw_skills::SkillManager mgr; - ASSERT_TRUE(mgr.agent() == nullptr); - signalwire::agent::AgentBase agent; - (void)mgr.load_skill("math", json::object(), agent); - sw_skills::SkillBase* skill = mgr.get_skill("math"); - ASSERT_TRUE(skill != nullptr); - ASSERT_TRUE(skill->agent() == &agent); - ASSERT_TRUE(skill->params().is_object()); - return true; + sw_skills::SkillManager mgr; + ASSERT_TRUE(mgr.agent() == nullptr); + ASSERT_FALSE(mgr.load_skill("math")); + ASSERT_FALSE(mgr.is_loaded("math")); + return true; } diff --git a/tests/test_swaig_secure_token.cpp b/tests/test_swaig_secure_token.cpp new file mode 100644 index 0000000..dbb37b3 --- /dev/null +++ b/tests/test_swaig_secure_token.cpp @@ -0,0 +1,443 @@ +// Copyright (c) 2025 SignalWire +// +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// `secure=true` SWAIG token enforcement, on EVERY transport. +// +// A tool registered with `secure=true` REQUIRES a valid `__token`. An ABSENT +// token is refused exactly like a forged one -- omitting the credential must +// never be weaker than presenting a wrong one, or `secure` would be a flag that +// permits anonymous calls. A missing `call_id` counts as UNVALIDATED (there is +// nothing to check the token against), never as a bypass. An `secure=false` +// tool runs ungated in all of those cases. +// +// The refusal is a 200 + FunctionResult body, NOT an HTTP error status: the +// engine has no handling for a SWAIG refusal status, so the tool reports that +// it cannot execute and the model relays that to the caller. +// +// The credential rides the QUERY STRING; the `call_id` rides the POST BODY. +// That split is identical on the HTTP endpoint and on every serverless mode, +// so serverless is not a weaker transport -- just a different envelope. + +#include + +#include +#include +#include +#include + +#include "signalwire/agent/agent_base.hpp" +#include "signalwire/common.hpp" +#include "signalwire/swaig/function_result.hpp" +#include "signalwire/utils/serverless.hpp" + +namespace { + +using json = nlohmann::json; +using signalwire::agent::AgentBase; +using signalwire::swaig::FunctionResult; + +constexpr const char* kSecUser = "tuser"; +constexpr const char* kSecPass = "tpass"; + +std::string sec_basic_auth(const std::string& u, const std::string& p) { + return "Basic " + signalwire::base64_encode(u + ":" + p); +} + +// An agent with one secure tool ("say_hello") and one insecure tool +// ("open_hello"), both returning a distinctive marker so a test can tell a RUN +// apart from a REFUSAL by the body alone. +class SecureTokenAgent : public AgentBase { + public: + SecureTokenAgent() : AgentBase("demo", "/demo") { + set_auth(kSecUser, kSecPass); + signalwire::swaig::ToolHandler handler = [](const json&, const json&) { + return FunctionResult("HANDLER RAN"); + }; + // define_tool defaults to secure=true. + define_tool("say_hello", "greet", json::object(), handler); + + signalwire::swaig::ToolDefinition open_tool; + open_tool.name = "open_hello"; + open_tool.description = "greet, ungated"; + open_tool.parameters = json::object(); + open_tool.secure = false; + open_tool.handler = handler; + define_tool(open_tool); + } + + // Expose the protected HTTP /swaig dispatcher so a test can drive the same + // handler the served route mounts, without standing up a real server. + void dispatch_swaig(const httplib::Request& req, httplib::Response& res) { + handle_swaig_request(req, res); + } +}; + +// Did the response body carry the handler's marker (the handler RAN)? +bool body_ran(const std::string& body) { + json parsed = json::parse(body, nullptr, false); + if (parsed.is_discarded() || !parsed.is_object()) { + return false; + } + return parsed.value("response", std::string()) == "HANDLER RAN"; +} + +// Did the response body carry the refusal (the handler did NOT run)? +bool body_refused(const std::string& body) { + json parsed = json::parse(body, nullptr, false); + if (parsed.is_discarded() || !parsed.is_object()) { + return false; + } + const std::string response = parsed.value("response", std::string()); + return response.find("security token") != std::string::npos; +} + +// Build the lambda event for one SWAIG call. `token` empty -> no query string +// at all; `call_id` empty -> the body carries no call_id key. +json lambda_event(const std::string& function_name, const std::string& token, + const std::string& call_id) { + json body = json::object(); + body["function"] = function_name; + body["argument"] = json{{"parsed", json::array({json::object()})}}; + if (!call_id.empty()) { + body["call_id"] = call_id; + } + json event = json::object(); + event["rawPath"] = "/swaig"; + event["headers"] = json{{"authorization", sec_basic_auth(kSecUser, kSecPass)}}; + event["body"] = body.dump(); + if (!token.empty()) { + event["queryStringParameters"] = json{{"__token", token}}; + } + return event; +} + +} // namespace + +// ============================================================================ +// SERVERLESS (lambda) -- valid / forged / absent / no-call-id, secure tool +// ============================================================================ + +TEST(swaig_secure_token_serverless_lambda_valid_token_runs) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "c1"); + ASSERT_FALSE(token.empty()); + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("say_hello", token, "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_lambda_forged_token_refused) { + SecureTokenAgent agent; + auto resp = signalwire::utils::handle_lambda( + agent, lambda_event("say_hello", "deadbeefdeadbeefdeadbeef", "c1")); + // 200 + FunctionResult refusal, NOT an HTTP error status. + ASSERT_EQ(resp.status, 200); + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_refused(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_lambda_absent_token_refused) { + SecureTokenAgent agent; + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("say_hello", "", "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_refused(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_lambda_absent_call_id_refused) { + SecureTokenAgent agent; + // A genuinely-minted token, but no call_id to validate it against. There is + // nothing to check it against, so it counts as unvalidated -- never a bypass. + const std::string token = agent.create_tool_token("say_hello", "c1"); + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("say_hello", token, "")); + ASSERT_EQ(resp.status, 200); + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_refused(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_lambda_token_for_another_call_refused) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "OTHER_CALL"); + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("say_hello", token, "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_refused(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_lambda_token_for_another_function_refused) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("open_hello", "c1"); + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("say_hello", token, "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_refused(resp.body)); + return true; +} + +// The HTTP API v2 raw query-string shape, as an alternative to the parsed +// `queryStringParameters` mapping. +TEST(swaig_secure_token_serverless_lambda_raw_query_string_accepted) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "c1"); + json event = lambda_event("say_hello", "", "c1"); + event["rawQueryString"] = "__token=" + token; + auto resp = signalwire::utils::handle_lambda(agent, event); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + return true; +} + +// The reference reads `__token` first and falls back to the bare `token`. +TEST(swaig_secure_token_serverless_lambda_bare_token_spelling_accepted) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "c1"); + json event = lambda_event("say_hello", "", "c1"); + event["queryStringParameters"] = json{{"token", token}}; + auto resp = signalwire::utils::handle_lambda(agent, event); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + return true; +} + +// ============================================================================ +// SERVERLESS (lambda) -- the INSECURE tool runs ungated in every case +// ============================================================================ + +TEST(swaig_secure_token_serverless_lambda_insecure_tool_runs_ungated) { + SecureTokenAgent agent; + const std::string good = agent.create_tool_token("open_hello", "c1"); + + // valid token + { + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("open_hello", good, "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + } + // forged token + { + auto resp = + signalwire::utils::handle_lambda(agent, lambda_event("open_hello", "deadbeef", "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + } + // absent token + { + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("open_hello", "", "c1")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + } + // absent call_id + { + auto resp = signalwire::utils::handle_lambda(agent, lambda_event("open_hello", "", "")); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_ran(resp.body)); + } + return true; +} + +// ============================================================================ +// SERVERLESS (lambda) -- path-based routing reaches the SAME check +// ============================================================================ + +TEST(swaig_secure_token_serverless_lambda_path_routing_enforced) { + SecureTokenAgent agent; + // Case 2 of the lambda dispatcher: the function name is the PATH, not a body + // key. It must reach the identical decision, or the check is bypassable by + // choosing the other routing shape. + json event = lambda_event("say_hello", "", "c1"); + event["rawPath"] = "/say_hello"; + auto resp = signalwire::utils::handle_lambda(agent, event); + ASSERT_EQ(resp.status, 200); + ASSERT_TRUE(body_refused(resp.body)); + + const std::string token = agent.create_tool_token("say_hello", "c1"); + json ok_event = lambda_event("say_hello", token, "c1"); + ok_event["rawPath"] = "/say_hello"; + auto ok = signalwire::utils::handle_lambda(agent, ok_event); + ASSERT_EQ(ok.status, 200); + ASSERT_TRUE(body_ran(ok.body)); + return true; +} + +// ============================================================================ +// HTTP -- the same four rows, over the httplib endpoint +// ============================================================================ + +namespace { + +// Drive the HTTP /swaig endpoint the way the served route does, through the +// public handler surface. Returns (status, body). +std::pair http_swaig(SecureTokenAgent& agent, const std::string& function_name, + const std::string& token, const std::string& call_id) { + json body = json::object(); + body["function"] = function_name; + body["argument"] = json{{"parsed", json::array({json::object()})}}; + if (!call_id.empty()) { + body["call_id"] = call_id; + } + httplib::Request req; + req.method = "POST"; + req.path = "/demo/swaig"; + req.body = body.dump(); + req.set_header("Authorization", sec_basic_auth(kSecUser, kSecPass)); + req.set_header("Content-Type", "application/json"); + if (!token.empty()) { + req.params.emplace("__token", token); + } + httplib::Response res; + agent.dispatch_swaig(req, res); + // httplib leaves `status` at its -1 sentinel when a handler never sets one + // and fills in 200 on the wire; model that here so the assertion is about + // the status the CALLER sees, not the sentinel. + return {res.status == -1 ? 200 : res.status, res.body}; +} + +} // namespace + +TEST(swaig_secure_token_http_valid_token_runs) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "c1"); + auto [status, body] = http_swaig(agent, "say_hello", token, "c1"); + ASSERT_EQ(status, 200); + ASSERT_TRUE(body_ran(body)); + return true; +} + +TEST(swaig_secure_token_http_forged_token_refused) { + SecureTokenAgent agent; + auto [status, body] = http_swaig(agent, "say_hello", "deadbeefdeadbeef", "c1"); + // 200 + FunctionResult refusal, NOT 403 -- the engine has no handling for a + // SWAIG refusal status, so a non-200 is dropped rather than relayed. + ASSERT_EQ(status, 200); + ASSERT_FALSE(body_ran(body)); + ASSERT_TRUE(body_refused(body)); + return true; +} + +TEST(swaig_secure_token_http_absent_token_refused) { + SecureTokenAgent agent; + auto [status, body] = http_swaig(agent, "say_hello", "", "c1"); + ASSERT_EQ(status, 200); + ASSERT_FALSE(body_ran(body)); + ASSERT_TRUE(body_refused(body)); + return true; +} + +TEST(swaig_secure_token_http_absent_call_id_refused) { + SecureTokenAgent agent; + const std::string token = agent.create_tool_token("say_hello", "c1"); + auto [status, body] = http_swaig(agent, "say_hello", token, ""); + ASSERT_EQ(status, 200); + ASSERT_FALSE(body_ran(body)); + ASSERT_TRUE(body_refused(body)); + return true; +} + +TEST(swaig_secure_token_http_insecure_tool_runs_ungated) { + SecureTokenAgent agent; + const std::string good = agent.create_tool_token("open_hello", "c1"); + { + auto [status, body] = http_swaig(agent, "open_hello", good, "c1"); + ASSERT_EQ(status, 200); + ASSERT_TRUE(body_ran(body)); + } + { + auto [status, body] = http_swaig(agent, "open_hello", "deadbeef", "c1"); + ASSERT_EQ(status, 200); + ASSERT_TRUE(body_ran(body)); + } + { + auto [status, body] = http_swaig(agent, "open_hello", "", "c1"); + ASSERT_EQ(status, 200); + ASSERT_TRUE(body_ran(body)); + } + { + auto [status, body] = http_swaig(agent, "open_hello", "", ""); + ASSERT_EQ(status, 200); + ASSERT_TRUE(body_ran(body)); + } + return true; +} + +// ============================================================================ +// CGI / GCF / AZURE -- these envelopes do not dispatch SWAIG at all +// ============================================================================ +// +// Unlike the lambda envelope (which has its own SWAIG dispatcher), cgi / gcf / +// azure route straight to `AgentBase::handle_request`, whose only outcomes are +// 401, a 307 routing-callback redirect, and a rendered SWML document. There is +// no SWAIG dispatch on those paths, so a tool handler is UNREACHABLE over them +// — which is why there is no token check to make: an unreachable handler cannot +// be reached without a credential either. +// +// These tests pin that property. If a future change adds SWAIG dispatch to +// `handle_request`, they go RED and the token check has to come with it — that +// is exactly the regression they exist to catch. + +namespace { + +// Does the body look like a rendered SWML document rather than a SWAIG result? +bool body_is_swml(const std::string& body) { + json parsed = json::parse(body, nullptr, false); + return !parsed.is_discarded() && parsed.is_object() && parsed.contains("sections"); +} + +} // namespace + +TEST(swaig_secure_token_serverless_cgi_does_not_dispatch_swaig) { + SecureTokenAgent agent; + json body = json{{"function", "say_hello"}, + {"argument", json{{"parsed", json::array({json::object()})}}}, + {"call_id", "c1"}}; + std::map env = { + {"REQUEST_METHOD", "POST"}, + {"PATH_INFO", "/say_hello"}, + {"CONTENT_TYPE", "application/json"}, + {"HTTP_AUTHORIZATION", sec_basic_auth(kSecUser, kSecPass)}}; + + auto resp = signalwire::utils::handle_cgi(agent, env, body.dump()); + ASSERT_EQ(resp.status, 200); + // The handler is unreachable over this envelope: SWML back, never the marker. + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_is_swml(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_gcf_does_not_dispatch_swaig) { + SecureTokenAgent agent; + json body = json{{"function", "say_hello"}, + {"argument", json{{"parsed", json::array({json::object()})}}}, + {"call_id", "c1"}}; + std::map headers = { + {"Authorization", sec_basic_auth(kSecUser, kSecPass)}, {"Content-Type", "application/json"}}; + + auto resp = signalwire::utils::handle_gcf(agent, "POST", "/say_hello", headers, body.dump()); + ASSERT_EQ(resp.status, 200); + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_is_swml(resp.body)); + return true; +} + +TEST(swaig_secure_token_serverless_azure_does_not_dispatch_swaig) { + SecureTokenAgent agent; + json body = json{{"function", "say_hello"}, + {"argument", json{{"parsed", json::array({json::object()})}}}, + {"call_id", "c1"}}; + json headers = json{{"Authorization", sec_basic_auth(kSecUser, kSecPass)}, + {"Content-Type", "application/json"}}; + + json req = json{{"method", "POST"}, + {"url", "https://fn.azurewebsites.net/api/say_hello"}, + {"headers", headers}, + {"body", body.dump()}}; + auto resp = signalwire::utils::handle_azure(agent, req); + ASSERT_EQ(resp.status, 200); + ASSERT_FALSE(body_ran(resp.body)); + ASSERT_TRUE(body_is_swml(resp.body)); + return true; +} diff --git a/tests/test_swml.cpp b/tests/test_swml.cpp index 9f8a0a2..8ee136e 100644 --- a/tests/test_swml.cpp +++ b/tests/test_swml.cpp @@ -1,9 +1,9 @@ // SWML Document, Schema, and Service tests +#include "signalwire/logging.hpp" #include "signalwire/swml/document.hpp" #include "signalwire/swml/schema.hpp" #include "signalwire/swml/service.hpp" -#include "signalwire/logging.hpp" using namespace signalwire::swml; using json = nlohmann::json; @@ -13,75 +13,75 @@ using json = nlohmann::json; // ======================================================================== TEST(document_default_version) { - Document doc; - auto j = doc.to_json(); - ASSERT_EQ(j["version"].get(), "1.0.0"); - return true; + Document doc; + auto j = doc.to_json(); + ASSERT_EQ(j["version"].get(), "1.0.0"); + return true; } TEST(document_has_main_section) { - Document doc; - auto j = doc.to_json(); - ASSERT_TRUE(j.contains("sections")); - ASSERT_TRUE(j["sections"].contains("main")); - ASSERT_TRUE(j["sections"]["main"].is_array()); - return true; + Document doc; + auto j = doc.to_json(); + ASSERT_TRUE(j.contains("sections")); + ASSERT_TRUE(j["sections"].contains("main")); + ASSERT_TRUE(j["sections"]["main"].is_array()); + return true; } TEST(document_add_verb) { - Document doc; - doc.add_verb("answer", json::object({{"max_duration", 3600}})); - auto j = doc.to_json(); - ASSERT_EQ(j["sections"]["main"].size(), 1u); - ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); - ASSERT_EQ(j["sections"]["main"][0]["answer"]["max_duration"].get(), 3600); - return true; + Document doc; + doc.add_verb("answer", json::object({{"max_duration", 3600}})); + auto j = doc.to_json(); + ASSERT_EQ(j["sections"]["main"].size(), 1u); + ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); + ASSERT_EQ(j["sections"]["main"][0]["answer"]["max_duration"].get(), 3600); + return true; } TEST(document_multiple_verbs) { - Document doc; - doc.add_verb("answer", json::object({{"max_duration", 3600}})); - doc.add_verb("hangup", json::object()); - auto j = doc.to_json(); - ASSERT_EQ(j["sections"]["main"].size(), 2u); - ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); - ASSERT_TRUE(j["sections"]["main"][1].contains("hangup")); - return true; + Document doc; + doc.add_verb("answer", json::object({{"max_duration", 3600}})); + doc.add_verb("hangup", json::object()); + auto j = doc.to_json(); + ASSERT_EQ(j["sections"]["main"].size(), 2u); + ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); + ASSERT_TRUE(j["sections"]["main"][1].contains("hangup")); + return true; } TEST(document_custom_section) { - Document doc; - doc.add_verb_to_section("custom", "play", json::object({{"url", "test.mp3"}})); - auto j = doc.to_json(); - ASSERT_TRUE(j["sections"].contains("custom")); - ASSERT_TRUE(j["sections"]["custom"][0].contains("play")); - return true; + Document doc; + doc.add_verb_to_section("custom", "play", json::object({{"url", "test.mp3"}})); + auto j = doc.to_json(); + ASSERT_TRUE(j["sections"].contains("custom")); + ASSERT_TRUE(j["sections"]["custom"][0].contains("play")); + return true; } TEST(document_has_section) { - Document doc; - ASSERT_TRUE(doc.has_section("main")); - ASSERT_FALSE(doc.has_section("nonexistent")); - doc.section("custom"); - ASSERT_TRUE(doc.has_section("custom")); - return true; + Document doc; + ASSERT_TRUE(doc.has_section("main")); + ASSERT_FALSE(doc.has_section("nonexistent")); + doc.section("custom"); + ASSERT_TRUE(doc.has_section("custom")); + return true; } TEST(document_to_string) { - Document doc; - doc.add_verb("answer", json::object()); - std::string s = doc.to_string(); - ASSERT_TRUE(s.find("answer") != std::string::npos); - ASSERT_TRUE(s.find("1.0.0") != std::string::npos); - return true; + Document doc; + doc.add_verb("answer", json::object()); + std::string s = doc.to_string(); + ASSERT_TRUE(s.find("answer") != std::string::npos); + ASSERT_TRUE(s.find("1.0.0") != std::string::npos); + return true; } TEST(document_set_version) { - Document doc; - doc.set_version("2.0.0"); - auto j = doc.to_json(); - ASSERT_EQ(j["version"].get(), "2.0.0"); - return true; + Document doc; + doc.set_version("2.0.0"); + auto j = doc.to_json(); + ASSERT_EQ(j["version"].get(), "2.0.0"); + return true; } // ======================================================================== @@ -89,84 +89,84 @@ TEST(document_set_version) { // ======================================================================== TEST(schema_load_embedded) { - Schema schema; - ASSERT_TRUE(schema.load_embedded()); - auto names = schema.verb_names(); - // Not a frozen headcount -- see schema_load_from_file. - ASSERT_TRUE(names.size() >= 38u); - return true; + Schema schema; + ASSERT_TRUE(schema.load_embedded()); + auto names = schema.verb_names(); + // Not a frozen headcount -- see schema_load_from_file. + ASSERT_TRUE(names.size() >= 38u); + return true; } TEST(schema_find_verb) { - Schema schema; - (void)schema.load_embedded(); - auto* vd = schema.find_verb("answer"); - ASSERT_TRUE(vd != nullptr); - ASSERT_EQ(vd->verb_name, "answer"); - ASSERT_EQ(vd->schema_name, "Answer"); - return true; + Schema schema; + (void)schema.load_embedded(); + auto* vd = schema.find_verb("answer"); + ASSERT_TRUE(vd != nullptr); + ASSERT_EQ(vd->verb_name, "answer"); + ASSERT_EQ(vd->schema_name, "Answer"); + return true; } TEST(schema_find_sip_refer) { - Schema schema; - (void)schema.load_embedded(); - auto* vd = schema.find_verb("sip_refer"); - ASSERT_TRUE(vd != nullptr); - ASSERT_EQ(vd->schema_name, "SIPRefer"); - return true; + Schema schema; + (void)schema.load_embedded(); + auto* vd = schema.find_verb("sip_refer"); + ASSERT_TRUE(vd != nullptr); + ASSERT_EQ(vd->schema_name, "SIPRefer"); + return true; } TEST(schema_find_nonexistent) { - Schema schema; - (void)schema.load_embedded(); - ASSERT_TRUE(schema.find_verb("nonexistent_verb") == nullptr); - return true; + Schema schema; + (void)schema.load_embedded(); + ASSERT_TRUE(schema.find_verb("nonexistent_verb") == nullptr); + return true; } TEST(schema_verb_names_contain_all_38) { - Schema schema; - (void)schema.load_embedded(); - auto names = schema.verb_names(); - - // Check some specific important verbs - auto has = [&](const std::string& n) { - return std::find(names.begin(), names.end(), n) != names.end(); - }; - - ASSERT_TRUE(has("answer")); - ASSERT_TRUE(has("ai")); - ASSERT_TRUE(has("hangup")); - ASSERT_TRUE(has("connect")); - ASSERT_TRUE(has("play")); - ASSERT_TRUE(has("record")); - ASSERT_TRUE(has("transfer")); - ASSERT_TRUE(has("sleep")); - ASSERT_TRUE(has("sip_refer")); - ASSERT_TRUE(has("detect_machine")); - ASSERT_TRUE(has("user_event")); - ASSERT_TRUE(has("amazon_bedrock")); - ASSERT_TRUE(has("live_transcribe")); - ASSERT_TRUE(has("live_translate")); - ASSERT_TRUE(has("enter_queue")); - ASSERT_TRUE(has("join_conference")); - ASSERT_TRUE(has("join_room")); - ASSERT_TRUE(has("pay")); - ASSERT_TRUE(has("send_sms")); - return true; + Schema schema; + (void)schema.load_embedded(); + auto names = schema.verb_names(); + + // Check some specific important verbs + auto has = [&](const std::string& n) { + return std::find(names.begin(), names.end(), n) != names.end(); + }; + + ASSERT_TRUE(has("answer")); + ASSERT_TRUE(has("ai")); + ASSERT_TRUE(has("hangup")); + ASSERT_TRUE(has("connect")); + ASSERT_TRUE(has("play")); + ASSERT_TRUE(has("record")); + ASSERT_TRUE(has("transfer")); + ASSERT_TRUE(has("sleep")); + ASSERT_TRUE(has("sip_refer")); + ASSERT_TRUE(has("detect_machine")); + ASSERT_TRUE(has("user_event")); + ASSERT_TRUE(has("amazon_bedrock")); + ASSERT_TRUE(has("live_transcribe")); + ASSERT_TRUE(has("live_translate")); + ASSERT_TRUE(has("enter_queue")); + ASSERT_TRUE(has("join_conference")); + ASSERT_TRUE(has("join_room")); + ASSERT_TRUE(has("pay")); + ASSERT_TRUE(has("send_sms")); + return true; } TEST(schema_load_from_file) { - Schema schema; - bool loaded = schema.load_from_file("src/swml/schema.json"); - if (loaded) { - auto names = schema.verb_names(); - // Not a frozen headcount: a literal here has to be edited by every PR that - // adds a verb upstream (ai_sidecar took it 38 -> 39) and never caught a real - // defect. What matters is that the load was not TRUNCATED. - ASSERT_TRUE(names.size() >= 38u); - } - // It's OK if file doesn't exist in CI - return true; + Schema schema; + bool loaded = schema.load_from_file("src/swml/schema.json"); + if (loaded) { + auto names = schema.verb_names(); + // Not a frozen headcount: a literal here has to be edited by every PR that + // adds a verb upstream (ai_sidecar took it 38 -> 39) and never caught a real + // defect. What matters is that the load was not TRUNCATED. + ASSERT_TRUE(names.size() >= 38u); + } + // It's OK if file doesn't exist in CI + return true; } // ======================================================================== @@ -174,128 +174,128 @@ TEST(schema_load_from_file) { // ======================================================================== TEST(service_default_route) { - Service svc; - ASSERT_EQ(svc.route(), "/"); - return true; + Service svc; + ASSERT_EQ(svc.route(), "/"); + return true; } TEST(service_set_route) { - Service svc; - svc.set_route("/agent"); - ASSERT_EQ(svc.route(), "/agent"); - return true; + Service svc; + svc.set_route("/agent"); + ASSERT_EQ(svc.route(), "/agent"); + return true; } TEST(service_set_route_prepends_slash) { - Service svc; - svc.set_route("agent"); - ASSERT_EQ(svc.route(), "/agent"); - return true; + Service svc; + svc.set_route("agent"); + ASSERT_EQ(svc.route(), "/agent"); + return true; } TEST(service_set_auth) { - Service svc; - svc.set_auth("user", "pass"); - ASSERT_EQ(svc.auth_username(), "user"); - ASSERT_EQ(svc.auth_password(), "pass"); - return true; + Service svc; + svc.set_auth("user", "pass"); + ASSERT_EQ(svc.auth_username(), "user"); + ASSERT_EQ(svc.auth_password(), "pass"); + return true; } TEST(service_verb_methods_add_to_document) { - Service svc; - svc.answer(json::object({{"max_duration", 3600}})); - svc.hangup(); - - auto j = svc.render_swml(); - ASSERT_EQ(j["sections"]["main"].size(), 2u); - ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); - ASSERT_TRUE(j["sections"]["main"][1].contains("hangup")); - return true; + Service svc; + svc.answer(json::object({{"max_duration", 3600}})); + svc.hangup(); + + auto j = svc.render_swml(); + ASSERT_EQ(j["sections"]["main"].size(), 2u); + ASSERT_TRUE(j["sections"]["main"][0].contains("answer")); + ASSERT_TRUE(j["sections"]["main"][1].contains("hangup")); + return true; } TEST(service_sleep_verb) { - Service svc; - svc.sleep(1000); - auto j = svc.render_swml(); - ASSERT_TRUE(j["sections"]["main"][0].contains("sleep")); - ASSERT_EQ(j["sections"]["main"][0]["sleep"].get(), 1000); - return true; + Service svc; + svc.sleep(1000); + auto j = svc.render_swml(); + ASSERT_TRUE(j["sections"]["main"][0].contains("sleep")); + ASSERT_EQ(j["sections"]["main"][0]["sleep"].get(), 1000); + return true; } TEST(service_all_verbs_exist) { - Service svc; - // Call each verb method to ensure it compiles and works - svc.answer(); - svc.ai(); - svc.amazon_bedrock(); - svc.cond(json::array()); - svc.connect(); - svc.denoise(); - svc.detect_machine(); - svc.enter_queue(); - svc.execute(); - svc.goto_section(); - svc.hangup(); - svc.join_conference(); - svc.join_room(); - svc.label(); - svc.live_transcribe(); - svc.live_translate(); - svc.pay(); - svc.play(); - svc.prompt(); - svc.receive_fax(); - svc.record(); - svc.record_call(); - svc.request(); - svc.return_section(); - svc.send_digits(); - svc.send_fax(); - svc.send_sms(); - svc.set(); - svc.sleep(500); - svc.sip_refer(); - svc.stop_denoise(); - svc.stop_record_call(); - svc.stop_tap(); - svc.switch_section(); - svc.tap(); - svc.transfer(); - svc.unset(); - svc.user_event(); - - auto j = svc.render_swml(); - // 37 explicit calls + 1 sleep = 38 verbs - ASSERT_EQ(j["sections"]["main"].size(), 38u); - return true; + Service svc; + // Call each verb method to ensure it compiles and works + svc.answer(); + svc.ai(); + svc.amazon_bedrock(); + svc.cond(json::array()); + svc.connect(); + svc.denoise(); + svc.detect_machine(); + svc.enter_queue(); + svc.execute(); + svc.goto_section(); + svc.hangup(); + svc.join_conference(); + svc.join_room(); + svc.label(); + svc.live_transcribe(); + svc.live_translate(); + svc.pay(); + svc.play(); + svc.prompt(); + svc.receive_fax(); + svc.record(); + svc.record_call(); + svc.request(); + svc.return_section(); + svc.send_digits(); + svc.send_fax(); + svc.send_sms(); + svc.set(); + svc.sleep(500); + svc.sip_refer(); + svc.stop_denoise(); + svc.stop_record_call(); + svc.stop_tap(); + svc.switch_section(); + svc.tap(); + svc.transfer(); + svc.unset(); + svc.user_event(); + + auto j = svc.render_swml(); + // 37 explicit calls + 1 sleep = 38 verbs + ASSERT_EQ(j["sections"]["main"].size(), 38u); + return true; } TEST(service_render_swml_json) { - Service svc; - svc.answer(json::object({{"max_duration", 7200}})); - auto j = svc.render_swml(); - ASSERT_EQ(j["version"].get(), "1.0.0"); - ASSERT_TRUE(j.contains("sections")); - return true; + Service svc; + svc.answer(json::object({{"max_duration", 7200}})); + auto j = svc.render_swml(); + ASSERT_EQ(j["version"].get(), "1.0.0"); + ASSERT_TRUE(j.contains("sections")); + return true; } TEST(service_timing_safe_compare) { - ASSERT_TRUE(Service::timing_safe_compare("hello", "hello")); - ASSERT_FALSE(Service::timing_safe_compare("hello", "world")); - ASSERT_FALSE(Service::timing_safe_compare("short", "longer_string")); - ASSERT_TRUE(Service::timing_safe_compare("", "")); - return true; + ASSERT_TRUE(Service::timing_safe_compare("hello", "hello")); + ASSERT_FALSE(Service::timing_safe_compare("hello", "world")); + ASSERT_FALSE(Service::timing_safe_compare("short", "longer_string")); + ASSERT_TRUE(Service::timing_safe_compare("", "")); + return true; } TEST(service_generate_random_hex) { - auto hex = Service::generate_random_hex(16); - ASSERT_EQ(hex.size(), 32u); // 16 bytes = 32 hex chars - // Verify all chars are hex - for (char c : hex) { - ASSERT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); - } - // Two random strings should differ - auto hex2 = Service::generate_random_hex(16); - ASSERT_NE(hex, hex2); - return true; + auto hex = Service::generate_random_hex(16); + ASSERT_EQ(hex.size(), 32u); // 16 bytes = 32 hex chars + // Verify all chars are hex + for (char c : hex) { + ASSERT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); + } + // Two random strings should differ + auto hex2 = Service::generate_random_hex(16); + ASSERT_NE(hex, hex2); + return true; } diff --git a/tests/test_swml_builder.cpp b/tests/test_swml_builder.cpp index aef433b..c9b4943 100644 --- a/tests/test_swml_builder.cpp +++ b/tests/test_swml_builder.cpp @@ -56,13 +56,20 @@ TEST(swml_builder_ai_pom_and_kwargs) { signalwire::swml::Service svc; SWMLBuilder b(svc); json pom = json::array({{{"title", "Role"}}}); - json kwargs = {{"temperature", 0.7}}; + // The kwargs merge at the ai TOP level (reference: `**(params or {})` into + // `builder.ai(...)`), so the key must be one `$defs/AIObject` actually + // declares — it is closed over nine keys via + // `unevaluatedProperties: {"not": {}}`. This fixture used `temperature`, which + // is in neither AIObject nor the 92-key `$defs/AIParams`; it rode the raw + // document path and produced a schema-invalid document. `global_data` is a + // real AIObject key. + json kwargs = {{"global_data", json::object({{"company_name", "Acme"}})}}; b.reset().ai(std::nullopt, std::optional(pom), std::nullopt, std::nullopt, std::nullopt, kwargs); json ai = find_main_verb(b.build(), "ai"); ASSERT_TRUE(ai["prompt"].contains("pom")); // kwargs merged at top level - ASSERT_EQ(ai["temperature"].get(), 0.7); + ASSERT_EQ(ai["global_data"]["company_name"].get(), std::string("Acme")); return true; } diff --git a/tests/test_swml_renderer.cpp b/tests/test_swml_renderer.cpp index 5467c15..ac5ae07 100644 --- a/tests/test_swml_renderer.cpp +++ b/tests/test_swml_renderer.cpp @@ -117,20 +117,108 @@ TEST(renderer_prompt_is_pom) { TEST(renderer_params_merged) { signalwire::swml::Service svc; RenderOptions opts; - opts.params = json::object({{"temperature", 0.3}}); + // RenderOptions::params spreads at the ai TOP level (reference: + // `**(params or {})`), so it must carry a key `$defs/AIObject` declares — the + // object is closed over nine keys. `temperature` is in neither AIObject nor + // `$defs/AIParams`; it rode the raw path into a schema-invalid document. + opts.params = json::object({{"hints", json::array({"acme"})}}); std::string s = SwmlRenderer::render_swml("Hi", svc, opts); json ai = first_main_verb(json::parse(s), "ai"); - ASSERT_EQ(ai["temperature"].get(), 0.3); + ASSERT_EQ(ai["hints"][0].get(), std::string("acme")); return true; } // ---- render_function_response_swml ---- -TEST(renderer_function_response_play_text) { +// Spoken text goes through the `say:` URL scheme. The SWML `play` verb has NO +// `text` key: schema.json defines it as oneOf[PlayWithURL, PlayWithURLS] with +// `unevaluatedProperties: {"not": {}}`, so `{"play": {"text": ...}}` is a +// document the schema rejects. The reference fixed exactly this +// (swml_renderer.py: `service.add_verb("play", {"url": f"say:{response_text}"})`) +// and SWMLBuilder::play next door only ever emits url/urls. +// +// Asserts on PARSED keys + value kinds, not a substring of the rendered blob: +// a `blob.find("All done") != npos` check passes for `text`, `url`, and any +// other shape, which is how this class of defect survives elsewhere. +TEST(renderer_function_response_play_uses_say_url_not_text) { signalwire::swml::Service svc; std::string s = SwmlRenderer::render_function_response_swml("All done", svc); json p = first_main_verb(json::parse(s), "play"); - ASSERT_EQ(p["text"].get(), std::string("All done")); + ASSERT_TRUE(p.is_object()); + // The canonical key, carrying the say: scheme. + ASSERT_TRUE(p.contains("url")); + ASSERT_TRUE(p["url"].is_string()); + ASSERT_EQ(p["url"].get(), std::string("say:All done")); + // The key the SWML play verb does not have, and that mod_infrastructure's + // schema rejects. + ASSERT_FALSE(p.contains("text")); + // Exactly one key -- nothing else smuggled in alongside. + ASSERT_EQ(p.size(), static_cast(1)); + return true; +} + +// A caller-supplied `play` action is passed through verbatim (the reference +// does `service.add_verb("play", action["play"])`), so a caller that already +// built a url/urls config is not rewritten. +TEST(renderer_function_response_play_action_passthrough) { + signalwire::swml::Service svc; + std::vector actions = { + json::object({{"play", json::object({{"url", "https://example.com/a.mp3"}})}})}; + std::string s = SwmlRenderer::render_function_response_swml( + "Spoken", svc, std::optional>(actions)); + json doc = json::parse(s); + // Two play verbs: the response text (as say:) then the action's URL. + std::vector plays; + for (const auto& v : doc["sections"]["main"]) { + if (v.contains("play")) { + plays.push_back(v["play"]); + } + } + ASSERT_EQ(plays.size(), static_cast(2)); + ASSERT_EQ(plays[0]["url"].get(), std::string("say:Spoken")); + ASSERT_FALSE(plays[0].contains("text")); + ASSERT_EQ(plays[1]["url"].get(), std::string("https://example.com/a.mp3")); + ASSERT_FALSE(plays[1].contains("text")); + return true; +} + +// The `ai` verb from the renderer must route through SWMLBuilder::ai, so +// prompt/post_prompt reach the wire as OBJECTS. mod_openai's app_config.c does +// `!cJSON_IsObject(prompt)` -> calling.error -> ABORTS THE CALL, so a bare +// string here is fatal on the wire, not cosmetic. +TEST(renderer_ai_prompt_and_post_prompt_are_objects_not_bare_strings) { + signalwire::swml::Service svc; + RenderOptions opts; + opts.post_prompt = "Summarize the call"; + std::string s = SwmlRenderer::render_swml("Be nice", svc, opts); + json ai = first_main_verb(json::parse(s), "ai"); + ASSERT_TRUE(ai.is_object()); + + ASSERT_TRUE(ai.contains("prompt")); + ASSERT_TRUE(ai["prompt"].is_object()); + ASSERT_FALSE(ai["prompt"].is_string()); + ASSERT_EQ(ai["prompt"]["text"].get(), std::string("Be nice")); + + ASSERT_TRUE(ai.contains("post_prompt")); + ASSERT_TRUE(ai["post_prompt"].is_object()); + ASSERT_FALSE(ai["post_prompt"].is_string()); + ASSERT_EQ(ai["post_prompt"]["text"].get(), std::string("Summarize the call")); + return true; +} + +// Same guarantee for the POM prompt shape: an ARRAY under `prompt.pom`, with +// `prompt` itself still an object. +TEST(renderer_ai_pom_prompt_is_object_wrapping_array) { + signalwire::swml::Service svc; + RenderOptions opts; + opts.prompt_is_pom = true; + json pom = json::array({{{"title", "Role"}, {"body", "Agent"}}}); + std::string s = SwmlRenderer::render_swml(pom, svc, opts); + json ai = first_main_verb(json::parse(s), "ai"); + ASSERT_TRUE(ai["prompt"].is_object()); + ASSERT_FALSE(ai["prompt"].is_string()); + ASSERT_TRUE(ai["prompt"]["pom"].is_array()); + ASSERT_FALSE(ai["prompt"].contains("text")); return true; } diff --git a/tests/test_swml_service_swaig.cpp b/tests/test_swml_service_swaig.cpp index 43d6db7..c0f66df 100644 --- a/tests/test_swml_service_swaig.cpp +++ b/tests/test_swml_service_swaig.cpp @@ -7,180 +7,173 @@ #include #include "httplib.h" -#include "signalwire/swml/service.hpp" #include "signalwire/swaig/function_result.hpp" +#include "signalwire/swml/service.hpp" using namespace signalwire::swml; using namespace signalwire::swaig; using json = nlohmann::json; TEST(service_has_swaig_methods) { - Service svc; - // These are smoke checks: if the methods didn't exist, this file - // wouldn't compile at all. - svc.set_name("svc"); - ASSERT_EQ(svc.name(), "svc"); - ASSERT_EQ(svc.list_tool_names().size(), 0u); - return true; + Service svc; + // These are smoke checks: if the methods didn't exist, this file + // wouldn't compile at all. + svc.set_name("svc"); + ASSERT_EQ(svc.name(), "svc"); + ASSERT_EQ(svc.list_tool_names().size(), 0u); + return true; } TEST(service_define_tool_dispatches_via_on_function_call) { - Service svc; - bool called = false; - json captured; - svc.define_tool( - "lookup", "Look it up", - json::object(), - [&](const json& args, const json&) -> FunctionResult { - called = true; - captured = args; - return FunctionResult("ok"); - }); - auto result = svc.on_function_call("lookup", json::object({{"x", "y"}}), json::object()); - ASSERT_TRUE(called); - ASSERT_EQ(captured["x"].get(), "y"); - auto out = result.to_json(); - ASSERT_EQ(out["response"].get(), "ok"); - return true; + Service svc; + bool called = false; + json captured; + svc.define_tool("lookup", "Look it up", json::object(), + [&](const json& args, const json&) -> FunctionResult { + called = true; + captured = args; + return FunctionResult("ok"); + }); + auto result = svc.on_function_call("lookup", json::object({{"x", "y"}}), json::object()); + ASSERT_TRUE(called); + ASSERT_EQ(captured["x"].get(), "y"); + auto out = result.to_json(); + ASSERT_EQ(out["response"].get(), "ok"); + return true; } TEST(service_on_function_call_returns_not_found_response_for_unknown) { - Service svc; - auto result = svc.on_function_call("no_such_fn", json::object(), json::object()); - auto out = result.to_json(); - ASSERT_TRUE(out["response"].get().find("not found") != std::string::npos); - return true; + Service svc; + auto result = svc.on_function_call("no_such_fn", json::object(), json::object()); + auto out = result.to_json(); + ASSERT_TRUE(out["response"].get().find("not found") != std::string::npos); + return true; } TEST(service_list_tool_names_returns_registered_order) { - Service svc; - svc.define_tool("first", "f", json::object(), [](const json&, const json&) { - return FunctionResult(); - }); - svc.define_tool("second", "s", json::object(), [](const json&, const json&) { - return FunctionResult(); - }); - auto names = svc.list_tool_names(); - ASSERT_EQ(names.size(), 2u); - ASSERT_EQ(names[0], "first"); - ASSERT_EQ(names[1], "second"); - return true; + Service svc; + svc.define_tool("first", "f", json::object(), + [](const json&, const json&) { return FunctionResult(); }); + svc.define_tool("second", "s", json::object(), + [](const json&, const json&) { return FunctionResult(); }); + auto names = svc.list_tool_names(); + ASSERT_EQ(names.size(), 2u); + ASSERT_EQ(names[0], "first"); + ASSERT_EQ(names[1], "second"); + return true; } TEST(service_register_swaig_function_tracks_in_order) { - Service svc; - svc.register_swaig_function(json::object({ - {"function", "datamap_tool"}, - {"description", "from data map"}, - })); - auto names = svc.list_tool_names(); - ASSERT_EQ(names.size(), 1u); - ASSERT_EQ(names[0], "datamap_tool"); - return true; + Service svc; + svc.register_swaig_function(json::object({ + {"function", "datamap_tool"}, + {"description", "from data map"}, + })); + auto names = svc.list_tool_names(); + ASSERT_EQ(names.size(), 1u); + ASSERT_EQ(names[0], "datamap_tool"); + return true; } // -------- Sidecar pattern: non-agent SWML + tool registration -------- TEST(service_sidecar_pattern_emits_verb_and_registers_tool) { - Service svc; - svc.set_name("sidecar").set_route("/sidecar"); - - // 1. Build the SWML — answer + ai_sidecar verb config. - svc.answer(); - svc.add_verb("main", "ai_sidecar", json::object({ - {"prompt", "real-time copilot"}, - {"lang", "en-US"}, - {"direction", json::array({"remote-caller", "local-caller"})}, - })); - - auto rendered = svc.render_swml(); - ASSERT_TRUE(rendered.contains("sections")); - auto& main = rendered["sections"]["main"]; - ASSERT_TRUE(main.is_array()); - bool has_answer = false, has_sidecar = false; - for (const auto& v : main) { - if (v.contains("answer")) has_answer = true; - if (v.contains("ai_sidecar")) has_sidecar = true; + Service svc; + svc.set_name("sidecar").set_route("/sidecar"); + + // 1. Build the SWML — answer + ai_sidecar verb config. + svc.answer(); + svc.add_verb("main", "ai_sidecar", + json::object({ + {"prompt", "real-time copilot"}, + {"lang", "en-US"}, + {"direction", json::array({"remote-caller", "local-caller"})}, + })); + + auto rendered = svc.render_swml(); + ASSERT_TRUE(rendered.contains("sections")); + auto& main = rendered["sections"]["main"]; + ASSERT_TRUE(main.is_array()); + bool has_answer = false, has_sidecar = false; + for (const auto& v : main) { + if (v.contains("answer")) { + has_answer = true; } - ASSERT_TRUE(has_answer); - ASSERT_TRUE(has_sidecar); - - // 2. Register a SWAIG tool the sidecar's LLM can call. - svc.define_tool( - "lookup_competitor", - "Look up competitor pricing.", - json::object({{"competitor", json::object({{"type", "string"}})}}), - [](const json& args, const json&) -> FunctionResult { - return FunctionResult( - args["competitor"].get() + " is $99/seat; we're $79." - ); - }); - - // 3. Dispatch end-to-end through on_function_call. - auto result = svc.on_function_call( - "lookup_competitor", - json::object({{"competitor", "ACME"}}), - json::object() - ); - auto out = result.to_json(); - auto resp = out["response"].get(); - ASSERT_TRUE(resp.find("ACME") != std::string::npos); - ASSERT_TRUE(resp.find("$79") != std::string::npos); - return true; + if (v.contains("ai_sidecar")) { + has_sidecar = true; + } + } + ASSERT_TRUE(has_answer); + ASSERT_TRUE(has_sidecar); + + // 2. Register a SWAIG tool the sidecar's LLM can call. + svc.define_tool( + "lookup_competitor", "Look up competitor pricing.", + json::object({{"competitor", json::object({{"type", "string"}})}}), + [](const json& args, const json&) -> FunctionResult { + return FunctionResult(args["competitor"].get() + " is $99/seat; we're $79."); + }); + + // 3. Dispatch end-to-end through on_function_call. + auto result = svc.on_function_call("lookup_competitor", json::object({{"competitor", "ACME"}}), + json::object()); + auto out = result.to_json(); + auto resp = out["response"].get(); + ASSERT_TRUE(resp.find("ACME") != std::string::npos); + ASSERT_TRUE(resp.find("$79") != std::string::npos); + return true; } TEST(service_build_tool_registry_json_dumps_runtime_registry) { - // build_tool_registry_json is the introspect helper the SDK's serve() - // calls when SWAIG_LIST_TOOLS=1 is set. It must produce - // {"tools":[]} in tool_order_, capturing - // whatever shape define_tool / register_swaig_function actually stored. - Service svc; - svc.define_tool(ToolDefinition{ - "lookup_competitor", - "Look up competitor pricing.", - json::object({ - {"type", "object"}, - {"properties", json::object({ - {"competitor", json::object({{"type", "string"}})}, - })}, - }), - [](const json&, const json&) -> FunctionResult { - return FunctionResult("ok"); - }, - false, - }); - svc.define_tool(ToolDefinition{ - "get_weather", - "Get the weather.", - json::object({{"type", "object"}}), - [](const json&, const json&) -> FunctionResult { return FunctionResult("sunny"); }, - false, - }); - - auto payload = svc.build_tool_registry_json(); - auto parsed = json::parse(payload); - ASSERT_TRUE(parsed.contains("tools")); - ASSERT_TRUE(parsed["tools"].is_array()); - ASSERT_EQ(parsed["tools"].size(), 2u); - ASSERT_EQ(parsed["tools"][0]["function"].get(), "lookup_competitor"); - ASSERT_EQ(parsed["tools"][1]["function"].get(), "get_weather"); - ASSERT_EQ(parsed["tools"][0]["description"].get(), "Look up competitor pricing."); - return true; + // build_tool_registry_json is the introspect helper the SDK's serve() + // calls when SWAIG_LIST_TOOLS=1 is set. It must produce + // {"tools":[]} in tool_order_, capturing + // whatever shape define_tool / register_swaig_function actually stored. + Service svc; + svc.define_tool(ToolDefinition{ + "lookup_competitor", + "Look up competitor pricing.", + json::object({ + {"type", "object"}, + {"properties", json::object({ + {"competitor", json::object({{"type", "string"}})}, + })}, + }), + [](const json&, const json&) -> FunctionResult { return FunctionResult("ok"); }, + false, + }); + svc.define_tool(ToolDefinition{ + "get_weather", + "Get the weather.", + json::object({{"type", "object"}}), + [](const json&, const json&) -> FunctionResult { return FunctionResult("sunny"); }, + false, + }); + + auto payload = svc.build_tool_registry_json(); + auto parsed = json::parse(payload); + ASSERT_TRUE(parsed.contains("tools")); + ASSERT_TRUE(parsed["tools"].is_array()); + ASSERT_EQ(parsed["tools"].size(), 2u); + ASSERT_EQ(parsed["tools"][0]["function"].get(), "lookup_competitor"); + ASSERT_EQ(parsed["tools"][1]["function"].get(), "get_weather"); + ASSERT_EQ(parsed["tools"][0]["description"].get(), "Look up competitor pricing."); + return true; } TEST(service_extract_introspect_payload_finds_json_between_sentinels) { - // The companion extractor used by the swaig-test --example CLI. - std::string captured = - "noise\n__SWAIG_TOOLS_BEGIN__\n{\"tools\":[]}\n__SWAIG_TOOLS_END__\nmore noise\n"; - auto payload = Service::extract_introspect_payload(captured); - ASSERT_EQ(payload, std::string("{\"tools\":[]}")); - return true; + // The companion extractor used by the swaig-test --example CLI. + std::string captured = + "noise\n__SWAIG_TOOLS_BEGIN__\n{\"tools\":[]}\n__SWAIG_TOOLS_END__\nmore noise\n"; + auto payload = Service::extract_introspect_payload(captured); + ASSERT_EQ(payload, std::string("{\"tools\":[]}")); + return true; } TEST(service_extract_introspect_payload_returns_empty_when_markers_missing) { - ASSERT_EQ(Service::extract_introspect_payload("no markers anywhere"), std::string()); - ASSERT_EQ(Service::extract_introspect_payload("__SWAIG_TOOLS_BEGIN__\n{}"), std::string()); - return true; + ASSERT_EQ(Service::extract_introspect_payload("no markers anywhere"), std::string()); + ASSERT_EQ(Service::extract_introspect_payload("__SWAIG_TOOLS_BEGIN__\n{}"), std::string()); + return true; } // as_router() — the cross-port "embed my routes in a host app" capability. @@ -188,57 +181,131 @@ TEST(service_extract_introspect_payload_returns_empty_when_markers_missing) { // FastAPI APIRouter; C++ returns a std::shared_ptr populated // with the service's routes but NOT bound to any port, for the caller to embed. TEST(service_as_router_returns_mountable_unpopulated_server) { - Service svc; - svc.set_name("router-svc"); - auto router = svc.as_router(); - // A real, valid, mountable Server (not null, not yet listening). - ASSERT_TRUE(router != nullptr); - ASSERT_TRUE(router->is_valid()); - ASSERT_FALSE(router->is_running()); - // Each call hands out a fresh unit the caller owns. - auto second = svc.as_router(); - ASSERT_NE(router.get(), second.get()); - return true; + Service svc; + svc.set_name("router-svc"); + auto router = svc.as_router(); + // A real, valid, mountable Server (not null, not yet listening). + ASSERT_TRUE(router != nullptr); + ASSERT_TRUE(router->is_valid()); + ASSERT_FALSE(router->is_running()); + // Each call hands out a fresh unit the caller owns. + auto second = svc.as_router(); + ASSERT_NE(router.get(), second.get()); + return true; } TEST(service_as_router_registers_the_services_routes) { - // Prove the returned Server actually carries the service's routes by - // mounting it on a free ephemeral port and driving it with an httplib - // client — the same "embed into a host" path a caller uses. Free-port - // discipline (MOCK_TEST_HARNESS): bind_to_any_port, never a fixed port. - Service svc; - svc.set_name("mounted-svc"); - - auto router = svc.as_router(); - int port = router->bind_to_any_port("127.0.0.1"); - ASSERT_TRUE(port > 0); - - std::thread server_thread([&router]() { router->listen_after_bind(); }); - // Wait for the server to accept connections. - for (int i = 0; i < 100 && !router->is_running(); ++i) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } + // Prove the returned Server actually carries the service's routes by + // mounting it on a free ephemeral port and driving it with an httplib + // client — the same "embed into a host" path a caller uses. Free-port + // discipline (MOCK_TEST_HARNESS): bind_to_any_port, never a fixed port. + Service svc; + svc.set_name("mounted-svc"); - bool health_ok = false; - bool swaig_ok = false; - { - httplib::Client cli("127.0.0.1", port); - cli.set_connection_timeout(2, 0); - // /health is registered by setup_routes and needs no auth. - auto health = cli.Get("/health"); - health_ok = (health != nullptr && health->status == 200); - // GET /swaig is registered by setup_routes and returns SWML (200) - // once the service's basic-auth credentials are supplied. - auto creds = svc.get_basic_auth_credentials(); - cli.set_basic_auth(creds.first, creds.second); - auto swaig = cli.Get("/swaig"); - swaig_ok = (swaig != nullptr && swaig->status == 200); - } + auto router = svc.as_router(); + int port = router->bind_to_any_port("127.0.0.1"); + ASSERT_TRUE(port > 0); + + std::thread server_thread([&router]() { router->listen_after_bind(); }); + // Wait for the server to accept connections. + for (int i = 0; i < 100 && !router->is_running(); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + bool health_ok = false; + bool swaig_ok = false; + { + httplib::Client cli("127.0.0.1", port); + cli.set_connection_timeout(2, 0); + // /health is registered by setup_routes and needs no auth. + auto health = cli.Get("/health"); + health_ok = (health != nullptr && health->status == 200); + // GET /swaig is registered by setup_routes and returns SWML (200) + // once the service's basic-auth credentials are supplied. + auto creds = svc.get_basic_auth_credentials(); + cli.set_basic_auth(creds.first, creds.second); + auto swaig = cli.Get("/swaig"); + swaig_ok = (swaig != nullptr && swaig->status == 200); + } + + router->stop(); + server_thread.join(); + + ASSERT_TRUE(health_ok); + ASSERT_TRUE(swaig_ok); + return true; +} + +// --------------------------------------------------------------------------- +// TLS / serving-domain values on the service itself. +// +// The reference copies these four off ``self.security`` in ``__init__``: +// self.ssl_enabled = self.security.ssl_enabled +// self.domain = self.security.domain +// self.ssl_cert_path = self.security.ssl_cert_path +// self.ssl_key_path = self.security.ssl_key_path +// and lets ``run()`` override them afterwards. They are caller-observable +// values on the SERVICE, not only on the SecurityConfig collaborator. +// --------------------------------------------------------------------------- + +namespace { +void clear_service_tls_env() { + ::unsetenv("SWML_SSL_ENABLED"); + ::unsetenv("SWML_SSL_CERT_PATH"); + ::unsetenv("SWML_SSL_KEY_PATH"); + ::unsetenv("SWML_DOMAIN"); +} +} // namespace + +TEST(service_tls_values_default_off) { + clear_service_tls_env(); + Service svc; + ASSERT_FALSE(svc.ssl_enabled()); + ASSERT_FALSE(svc.domain().has_value()); + ASSERT_FALSE(svc.ssl_cert_path().has_value()); + ASSERT_FALSE(svc.ssl_key_path().has_value()); + return true; +} + +// The ctor must SEED these from SecurityConfig — that is the reference's +// wiring, and it is what makes SWML_SSL_* reach the service at all. +TEST(service_tls_values_seeded_from_security_config) { + clear_service_tls_env(); + ::setenv("SWML_SSL_ENABLED", "true", 1); + ::setenv("SWML_SSL_CERT_PATH", "/etc/ssl/seeded.crt", 1); + ::setenv("SWML_SSL_KEY_PATH", "/etc/ssl/seeded.key", 1); + ::setenv("SWML_DOMAIN", "seeded.example.com", 1); + + Service svc; + bool enabled = svc.ssl_enabled(); + std::string cert = svc.ssl_cert_path().value_or(""); + std::string key = svc.ssl_key_path().value_or(""); + std::string dom = svc.domain().value_or(""); + + clear_service_tls_env(); + + ASSERT_TRUE(enabled); + ASSERT_EQ(cert, std::string("/etc/ssl/seeded.crt")); + ASSERT_EQ(key, std::string("/etc/ssl/seeded.key")); + ASSERT_EQ(dom, std::string("seeded.example.com")); + return true; +} + +// ...and an explicit setter overrides the seeded value, mirroring the +// reference's ``run(ssl_enabled=…, domain=…, ssl_cert=…, ssl_key=…)``. +TEST(service_tls_values_settable_after_construction) { + clear_service_tls_env(); + Service svc; + ASSERT_FALSE(svc.ssl_enabled()); - router->stop(); - server_thread.join(); + svc.set_ssl_enabled(true) + .set_ssl_cert_path("/tmp-unused/override.crt") + .set_ssl_key_path("/tmp-unused/override.key") + .set_domain("override.example.com"); - ASSERT_TRUE(health_ok); - ASSERT_TRUE(swaig_ok); - return true; + ASSERT_TRUE(svc.ssl_enabled()); + ASSERT_EQ(svc.ssl_cert_path().value_or(""), std::string("/tmp-unused/override.crt")); + ASSERT_EQ(svc.ssl_key_path().value_or(""), std::string("/tmp-unused/override.key")); + ASSERT_EQ(svc.domain().value_or(""), std::string("override.example.com")); + return true; } diff --git a/tests/test_swml_validating_path.cpp b/tests/test_swml_validating_path.cpp new file mode 100644 index 0000000..bf74cab --- /dev/null +++ b/tests/test_swml_validating_path.cpp @@ -0,0 +1,292 @@ +// test_swml_validating_path.cpp — ITEM 4 (task #194): every SWML verb the SDK +// emits on a caller's behalf must go through the VALIDATING entry point. +// +// The C++ port has four ways to put a verb into a document, and only ONE of +// them consults the schema: +// +// VALIDATING swml::Service::add_verb(verb_name, config) service.cpp:289 +// RAW swml::Service::add_verb(section, verb, params) service.cpp:283 +// RAW swml::Service::add_verb_to_section(...) service.cpp:397 +// RAW swml::Document::add_verb / Section::add_verb document.hpp:33/73 +// +// The 3-arg Service::add_verb SHARES ITS NAME with the validating 2-arg form +// and delegates straight to document_.add_verb_to_section with no schema check +// at all, so `service.add_verb(...)` is validating or not depending purely on +// arity. That naming trap is why `play {"text": ...}` (task #180) shipped in +// five ports: the validating path rejects it, and nothing ever went through the +// validating path. +// +// These tests assert THROUGH the validator rather than against a literal blob, +// so the next wrong key is caught too. + +#include +#include + +#include "signalwire/agent/agent_base.hpp" +#include "signalwire/core/swml_builder.hpp" +#include "signalwire/core/swml_renderer.hpp" +#include "signalwire/swml/service.hpp" +#include "signalwire/utils/schema_utils.hpp" + +using json = nlohmann::json; + +namespace { + +// True iff pushing (name, config) through the VALIDATING Service::add_verb +// does NOT throw. This is the proof-by-execution the brief asks for: we do not +// reason about whether a shape survives the schema, we run it. +bool vp_survives_validator(const std::string& name, const json& config) { + try { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + svc.add_verb(name, config); + return true; + } catch (...) { + return false; + } +} + +// Walk every verb in every section of a rendered SWML document back through the +// validating entry point. A document produced by any SDK-owned emitter must be +// re-constructible on the validating path; if it is not, the emitter shipped a +// shape the schema rejects. +bool vp_document_survives_validator(const json& doc) { + if (!doc.is_object() || !doc.contains("sections")) { + return false; + } + const json& sections = doc.at("sections"); + for (auto sec = sections.begin(); sec != sections.end(); ++sec) { + if (!sec.value().is_array()) { + continue; + } + for (const auto& verb : sec.value()) { + if (!verb.is_object()) { + return false; + } + for (auto it = verb.begin(); it != verb.end(); ++it) { + if (!vp_survives_validator(it.key(), it.value())) { + return false; + } + } + } + } + return true; +} + +signalwire::swaig::FunctionResult vp_noop_handler(const json&, const json&) { + return signalwire::swaig::FunctionResult("ok"); +} + +} // namespace + +// ============================================================================ +// The naming trap: 3-arg Service::add_verb must validate like the 2-arg form +// ============================================================================ + +TEST(validating_path_service_three_arg_add_verb_rejects_unknown_verb) { + // service.add_verb("main", "foobar", {}) previously wrote straight into the + // document. The caller cannot tell from the name that they lost validation. + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + ASSERT_THROWS(svc.add_verb("main", "foobar", json::object())); + return true; +} + +TEST(validating_path_service_three_arg_add_verb_rejects_bad_config) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + // `play` has no `text` key (task #180) — the schema rejects it. + ASSERT_THROWS(svc.add_verb("main", "play", json::object({{"text", "hi"}}))); + return true; +} + +TEST(validating_path_service_three_arg_add_verb_accepts_valid) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + try { + svc.add_verb("main", "play", json::object({{"url", "say:hello"}})); + } catch (...) { + return false; + } + json doc = svc.render_swml(); + ASSERT_TRUE(doc.at("sections").contains("main")); + return true; +} + +TEST(validating_path_service_add_verb_to_section_rejects_unknown_verb) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + ASSERT_THROWS(svc.add_verb_to_section("main", "foobar", json::object())); + return true; +} + +TEST(validating_path_service_add_verb_to_section_rejects_bad_config) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + ASSERT_THROWS(svc.add_verb_to_section("main", "play", json::object({{"text", "hi"}}))); + return true; +} + +// ============================================================================ +// SWMLBuilder — every builder method must land on the validating path +// ============================================================================ + +TEST(validating_path_builder_answer_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.answer(); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +TEST(validating_path_builder_answer_with_options_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.answer(3600, std::string("PCMU")); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +TEST(validating_path_builder_hangup_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.hangup(std::string("busy")); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +TEST(validating_path_builder_hangup_unknown_key_rejected) { + // The builder now reaches the validator, so a misspelled hangup key throws + // where it previously rode the raw path into the document. + // + // NOTE — a SEPARATE, PRE-EXISTING gap, deliberately not asserted here: + // ``$defs/Hangup.reason`` is a CLOSED enum (``anyOf`` of ``const`` + // hangup|busy|decline) and the Python reference REJECTS + // ``hangup {reason: "done"}``, but this port's ``validate_verb_full`` does not + // enforce ``anyOf``/``const`` VALUES — only key names and coarse types. That + // is a validator-depth gap, not a bypass gap, and fixing it is out of scope + // for the ITEM-4 routing change; asserting it here would red on a defect this + // commit does not claim to fix. + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + ASSERT_THROWS(svc.add_verb("hangup", json::object({{"raeson", "busy"}}))); + return true; +} + +TEST(validating_path_builder_play_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.play(std::string("https://example.com/a.wav")); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +TEST(validating_path_builder_say_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.say("hello there"); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +TEST(validating_path_builder_ai_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::SWMLBuilder b(svc); + b.ai(std::string("you are a helpful assistant")); + ASSERT_TRUE(vp_document_survives_validator(svc.render_swml())); + return true; +} + +// ============================================================================ +// SwmlRenderer — the record_call / play / action-verb emissions +// ============================================================================ + +TEST(validating_path_renderer_record_call_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + signalwire::core::RenderOptions opts; + opts.record_call = true; + std::string s = + signalwire::core::SwmlRenderer::render_swml(json::object({{"text", "hi"}}), svc, opts); + ASSERT_TRUE(vp_document_survives_validator(json::parse(s))); + return true; +} + +TEST(validating_path_renderer_function_response_play_survives_validator) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + std::string s = signalwire::core::SwmlRenderer::render_function_response_swml( + "spoken response", svc, std::nullopt, "json"); + json doc = json::parse(s); + ASSERT_TRUE(vp_document_survives_validator(doc)); + return true; +} + +TEST(validating_path_renderer_function_response_rejects_invalid_action) { + // An action verb handed in by a caller now goes through the validator, so a + // schema-forbidden shape fails loud instead of shipping. + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + std::vector actions = {json::object({{"play", json::object({{"text", "nope"}})}})}; + ASSERT_THROWS( + signalwire::core::SwmlRenderer::render_function_response_swml("", svc, actions, "json")); + return true; +} + +TEST(validating_path_renderer_function_response_valid_action_ok) { + signalwire::swml::Service svc; + svc.set_name("s").set_route("/s"); + std::vector actions = {json::object({{"hangup", json::object()}})}; + std::string s = + signalwire::core::SwmlRenderer::render_function_response_swml("", svc, actions, "json"); + ASSERT_TRUE(vp_document_survives_validator(json::parse(s))); + return true; +} + +// ============================================================================ +// AgentBase::render_swml_internal — the document it hand-builds had NO owner +// Service at all, so nothing could ever have validated it. +// ============================================================================ + +TEST(validating_path_agent_rendered_document_survives_validator) { + signalwire::agent::AgentBase a("a", "/a"); + a.set_prompt_text("you are a helpful assistant"); + json doc = a.render_swml(); + ASSERT_TRUE(vp_document_survives_validator(doc)); + return true; +} + +TEST(validating_path_agent_with_record_call_survives_validator) { + // record_call is a constructor argument, not a setter. + signalwire::agent::AgentBase a("a", "/a", "0.0.0.0", std::nullopt, std::nullopt, true, 3600, + /*auto_answer=*/true, /*record_call=*/true); + a.set_prompt_text("hello"); + json doc = a.render_swml(); + ASSERT_TRUE(vp_document_survives_validator(doc)); + return true; +} + +TEST(validating_path_agent_rejects_invalid_extra_verb) { + // A caller-supplied pre-answer verb is now validated at render time rather + // than being copied verbatim into the document. + signalwire::agent::AgentBase a("a", "/a"); + a.set_prompt_text("hello"); + a.add_pre_answer_verb("play", json::object({{"text", "nope"}})); + ASSERT_THROWS(a.render_swml()); + return true; +} + +TEST(validating_path_agent_accepts_valid_extra_verb) { + signalwire::agent::AgentBase a("a", "/a"); + a.set_prompt_text("hello"); + a.add_pre_answer_verb("play", json::object({{"url", "say:one moment"}})); + json doc = a.render_swml(); + ASSERT_TRUE(vp_document_survives_validator(doc)); + return true; +} diff --git a/tests/test_tier2_behavioral.cpp b/tests/test_tier2_behavioral.cpp index 57fdfb8..e49ddfb 100644 --- a/tests/test_tier2_behavioral.cpp +++ b/tests/test_tier2_behavioral.cpp @@ -19,7 +19,10 @@ #include #include +#include #include +#include +#include #include #include @@ -36,15 +39,31 @@ namespace { int tier2_pick_free_port() { int fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + std::cerr << "pick_free_port: socket() failed: " << std::strerror(errno) << "\n"; + return -1; + } sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; - ::bind(fd, reinterpret_cast(&addr), sizeof(addr)); + if (::bind(fd, reinterpret_cast(&addr), sizeof(addr)) != 0) { + std::cerr << "pick_free_port: bind() failed: " << std::strerror(errno) << "\n"; + ::close(fd); + return -1; + } socklen_t len = sizeof(addr); - ::getsockname(fd, reinterpret_cast(&addr), &len); + if (::getsockname(fd, reinterpret_cast(&addr), &len) != 0) { + std::cerr << "pick_free_port: getsockname() failed: " << std::strerror(errno) << "\n"; + ::close(fd); + return -1; + } int port = ntohs(addr.sin_port); ::close(fd); + if (port <= 0) { + std::cerr << "pick_free_port: kernel assigned no port\n"; + return -1; + } return port; } @@ -120,14 +139,14 @@ TEST(tier2_info_gatherer_submit_answer_advances_state) { }); // Simulate the SWAIG runtime handing back global_data at index 0. - json raw = json({{"global_data", - json({{"questions", - json::array({json({{"key_name", "name"}, - {"question_text", "What is your name?"}}), - json({{"key_name", "city"}, - {"question_text", "What city are you in?"}})})}, - {"question_index", 0}, - {"answers", json::array()}})}}); + json raw = json( + {{"global_data", + json({{"questions", + json::array( + {json({{"key_name", "name"}, {"question_text", "What is your name?"}}), + json({{"key_name", "city"}, {"question_text", "What city are you in?"}})})}, + {"question_index", 0}, + {"answers", json::array()}})}}); auto result = agent.submit_answer(json({{"answer", "Ada"}}), raw).to_json(); @@ -169,12 +188,15 @@ TEST(tier2_native_vector_search_remote_http_post) { try { json body = json::parse(req.body); captured_query = body.value("query", ""); - } catch (...) { + } catch (const json::exception& e) { + // Swallowing this left captured_query empty and the assertion below + // failed with no hint that the body had simply not parsed. + std::cerr << "mock /search: body did not parse as JSON (" << e.what() << ")\n"; } - json out = json({{"results", - json::array({json({{"content", "The capital of France is Paris."}, - {"score", 0.97}, - {"metadata", json({{"filename", "geo.md"}})}})})}}); + json out = + json({{"results", json::array({json({{"content", "The capital of France is Paris."}, + {"score", 0.97}, + {"metadata", json({{"filename", "geo.md"}})}})})}}); res.set_content(out.dump(), "application/json"); }); @@ -189,7 +211,9 @@ TEST(tier2_native_vector_search_remote_http_post) { std::thread& t; ~Guard() { s.stop(); - if (t.joinable()) t.join(); + if (t.joinable()) { + t.join(); + } } } guard{srv, server_thread}; @@ -298,6 +322,7 @@ TEST(tier2_sip_routing_served_dispatch) { ::unsetenv("PORT"); int port = tier2_pick_free_port(); + ASSERT_TRUE(port > 0); signalwire::agent::AgentBase agent("support", "/"); agent.set_host("127.0.0.1").set_port(port); @@ -310,7 +335,9 @@ TEST(tier2_sip_routing_served_dispatch) { std::thread& t; ~Guard() { a.stop(); - if (t.joinable()) t.join(); + if (t.joinable()) { + t.join(); + } ::unsetenv("SWML_BASIC_AUTH_USER"); ::unsetenv("SWML_BASIC_AUTH_PASSWORD"); } diff --git a/tests/test_tls_relay_no_downgrade.cpp b/tests/test_tls_relay_no_downgrade.cpp new file mode 100644 index 0000000..c9af059 --- /dev/null +++ b/tests/test_tls_relay_no_downgrade.cpp @@ -0,0 +1,91 @@ +// Copyright (c) 2025 SignalWire +// SPDX-License-Identifier: MIT +// +// TLS security test (#90, the silent-downgrade shape): setting +// SIGNALWIRE_RELAY_CA_FILE is an explicit request to VERIFY the RELAY peer +// against that CA, which is meaningless without TLS. If the transport then +// resolves to plain `ws://` -- a stale SIGNALWIRE_RELAY_SCHEME, a harness +// export leaking out of a test run, an operator who changed one setting and not +// the other -- the caller asked for encryption, gets NONE, and is never told. +// +// The RELAY client must REFUSE that combination and name the setting that would +// otherwise have been silently ignored, rather than completing a plaintext +// session. Same guard rust already ships (signalwire-rust +// src/relay/client.rs: "NO SILENT DOWNGRADE"). +// +// Behavioral, against the real plain-ws mock: with the CA var set and the +// scheme forced to ws, connect() must fail; with the CA var UNSET the very same +// plaintext connect must still SUCCEED (the negative control that keeps this +// test from passing merely because the mock is unreachable, and that pins the +// guard to the CA-var condition rather than to plaintext in general). + +#include +#include + +#include "relay_mocktest.hpp" +#include "signalwire/relay/client.hpp" + +// #included into the single test_main.cpp TU -> no file-scope `using +// namespace`; targeted declarations only. +namespace rmt = signalwire::relay::mocktest; +using signalwire::relay::RelayClient; +using signalwire::relay::RelayConfig; + +TEST(tls_relay_ca_file_refuses_plaintext_downgrade) { + try { + (void)rmt::ensure_server(); + } catch (const std::exception& e) { + std::cerr << "(skipped: mock_relay not reachable: " << e.what() << ") "; + return true; + } + + // Snapshot + restore the two globals this case mutates, so it cannot leak + // into any other test (the runner puts env-mutating cases in the serial + // batch, but a leaked CA var would still poison later plaintext cases). + const char* prev_ca_raw = std::getenv("SIGNALWIRE_RELAY_CA_FILE"); + const std::string prev_ca = (prev_ca_raw != nullptr) ? prev_ca_raw : std::string(); + const bool had_ca = (prev_ca_raw != nullptr); + + rmt::force_ws_scheme(); // SIGNALWIRE_RELAY_SCHEME=ws -> plain transport + + auto make_cfg = []() { + RelayConfig cfg; + cfg.project = "test_proj"; + cfg.token = "test_tok"; + cfg.host = "127.0.0.1"; + cfg.port = rmt::resolve_ws_port(); + cfg.contexts = {"default"}; + return cfg; + }; + + // (1) CONTROL: plaintext with NO CA request must still connect. This proves + // the mock is up and the plain path works, so the assertion below can + // only fail for the reason it is testing. + ::unsetenv("SIGNALWIRE_RELAY_CA_FILE"); + { + RelayClient control(make_cfg()); + bool ok = control.connect(); + ASSERT_TRUE(ok); + ASSERT_TRUE(control.is_connected()); + control.disconnect(); + } + + // (2) THE GUARD: with SIGNALWIRE_RELAY_CA_FILE set, the identical plaintext + // connect must be REFUSED rather than silently completing in the clear. + // The CA path need not exist -- the refusal is about the REQUEST for + // verification, which a plaintext transport can never honour. + ::setenv("SIGNALWIRE_RELAY_CA_FILE", "/nonexistent/ca-bundle.pem", 1); + { + RelayClient guarded(make_cfg()); + bool ok = guarded.connect(); + ASSERT_FALSE(ok); // must NOT downgrade to plaintext + ASSERT_FALSE(guarded.is_connected()); + } + + if (had_ca) { + ::setenv("SIGNALWIRE_RELAY_CA_FILE", prev_ca.c_str(), 1); + } else { + ::unsetenv("SIGNALWIRE_RELAY_CA_FILE"); + } + return true; +} diff --git a/tests/test_tls_relay_wss.cpp b/tests/test_tls_relay_wss.cpp index b921b80..82c0a9f 100644 --- a/tests/test_tls_relay_wss.cpp +++ b/tests/test_tls_relay_wss.cpp @@ -22,81 +22,80 @@ // WebSocketClient with an EMPTY trust store and asserts the handshake is // rejected, proving the cert is actually verified. -#include "tls_mocktest.hpp" +#include + #include "signalwire/relay/client.hpp" #include "signalwire/relay/websocket.hpp" - -#include +#include "tls_mocktest.hpp" // #included into the single test_main.cpp TU -> avoid file-scope // `using namespace`; use targeted declarations instead. namespace tt = signalwire::tlstest; -using signalwire::relay::RelayConfig; +using nlohmann::json; using signalwire::relay::RelayClient; +using signalwire::relay::RelayConfig; using signalwire::relay::WebSocketClient; -using nlohmann::json; TEST(tls_relay_client_wss_connect_authenticate) { - if (tt::ca_cert_path().empty() || !tt::relay_tls_available()) { - // TLS mock not reachable / certs missing -> skip cleanly (infra), the - // same discipline as the conftest mock-discovery skip. CI runs the - // --tls mock so the assertions below actually execute. - std::cerr << "(skipped: mock_relay --tls not reachable on " - << tt::relay_tls_http_url() << ") "; - return true; - } + if (tt::ca_cert_path().empty() || !tt::relay_tls_available()) { + // TLS mock not reachable / certs missing -> skip cleanly (infra), the + // same discipline as the conftest mock-discovery skip. CI runs the + // --tls mock so the assertions below actually execute. + std::cerr << "(skipped: mock_relay --tls not reachable on " << tt::relay_tls_http_url() << ") "; + return true; + } - tt::trust_test_ca(); // SSL_CERT_FILE -> test CA, before any dial - tt::relay_journal_reset(); + tt::trust_test_ca(); // SSL_CERT_FILE -> test CA, before any dial + tt::relay_journal_reset(); - // Production TLS path: leave SIGNALWIRE_RELAY_SCHEME UNSET so connect() - // routes through WebSocketClient::connect() (wss://), not connect_plain(). - ::unsetenv("SIGNALWIRE_RELAY_SCHEME"); + // Production TLS path: leave SIGNALWIRE_RELAY_SCHEME UNSET so connect() + // routes through WebSocketClient::connect() (wss://), not connect_plain(). + ::unsetenv("SIGNALWIRE_RELAY_SCHEME"); - RelayConfig cfg; - cfg.project = "test_proj"; - cfg.token = "test_tok"; - // Connect by the DNS name the test cert was issued for (SAN DNS:localhost, - // resolves to 127.0.0.1 via /etc/hosts). TLS hostname verification matches - // against DNS SANs, not bare IP literals — this is the production pattern - // (you reach a TLS endpoint by its certificate name, not its IP). - cfg.host = "localhost"; - cfg.port = tt::relay_tls_ws_port(); - cfg.contexts = {"default"}; + RelayConfig cfg; + cfg.project = "test_proj"; + cfg.token = "test_tok"; + // Connect by the DNS name the test cert was issued for (SAN DNS:localhost, + // resolves to 127.0.0.1 via /etc/hosts). TLS hostname verification matches + // against DNS SANs, not bare IP literals — this is the production pattern + // (you reach a TLS endpoint by its certificate name, not its IP). + cfg.host = "localhost"; + cfg.port = tt::relay_tls_ws_port(); + cfg.contexts = {"default"}; - RelayClient client(cfg); - bool ok = client.connect(); - ASSERT_TRUE(ok); // connect+authenticate completed over TLS - ASSERT_TRUE(client.is_connected()); + RelayClient client(cfg); + bool ok = client.connect(); + ASSERT_TRUE(ok); // connect+authenticate completed over TLS + ASSERT_TRUE(client.is_connected()); - // Behavioral proof the TLS session carried a real RELAY handshake: the - // mock only issues a protocol string on a successful credential exchange. - std::string proto = client.relay_protocol(); - ASSERT_FALSE(proto.empty()); - ASSERT_TRUE(proto.find("signalwire") != std::string::npos); + // Behavioral proof the TLS session carried a real RELAY handshake: the + // mock only issues a protocol string on a successful credential exchange. + std::string proto = client.relay_protocol(); + ASSERT_FALSE(proto.empty()); + ASSERT_TRUE(proto.find("signalwire") != std::string::npos); - // Wire proof: the mock journaled the inbound signalwire.connect frame on - // the same (TLS) WebSocket, carrying our credentials. - auto recvs = tt::relay_journal_recv("signalwire.connect"); - ASSERT_FALSE(recvs.empty()); - json auth = recvs.back()["frame"]["params"]["authentication"]; - ASSERT_EQ(auth.value("project", std::string("x")), std::string("test_proj")); - ASSERT_EQ(auth.value("token", std::string("x")), std::string("test_tok")); + // Wire proof: the mock journaled the inbound signalwire.connect frame on + // the same (TLS) WebSocket, carrying our credentials. + auto recvs = tt::relay_journal_recv("signalwire.connect"); + ASSERT_FALSE(recvs.empty()); + json auth = recvs.back()["frame"]["params"]["authentication"]; + ASSERT_EQ(auth.value("project", std::string("x")), std::string("test_proj")); + ASSERT_EQ(auth.value("token", std::string("x")), std::string("test_tok")); - client.disconnect(); - ASSERT_FALSE(client.is_connected()); + client.disconnect(); + ASSERT_FALSE(client.is_connected()); - // Negative control: the same wss:// endpoint must reject a client that does - // NOT trust the test CA, proving real certificate verification is in force. - // Point SSL_CERT_FILE at a path with no valid CA (the server cert itself is - // not a CA for itself under default verification) so the chain can't build. - { - ::setenv("SSL_CERT_FILE", "/dev/null", 1); // empty/invalid trust store - WebSocketClient raw; - bool neg_ok = raw.connect("localhost", tt::relay_tls_ws_port()); - ASSERT_FALSE(neg_ok); // handshake must fail (cert unverifiable) - ASSERT_FALSE(raw.is_connected()); - tt::trust_test_ca(); // restore trust for any later tests - } - return true; + // Negative control: the same wss:// endpoint must reject a client that does + // NOT trust the test CA, proving real certificate verification is in force. + // Point SSL_CERT_FILE at a path with no valid CA (the server cert itself is + // not a CA for itself under default verification) so the chain can't build. + { + ::setenv("SSL_CERT_FILE", "/dev/null", 1); // empty/invalid trust store + WebSocketClient raw; + bool neg_ok = raw.connect("localhost", tt::relay_tls_ws_port()); + ASSERT_FALSE(neg_ok); // handshake must fail (cert unverifiable) + ASSERT_FALSE(raw.is_connected()); + tt::trust_test_ca(); // restore trust for any later tests + } + return true; } diff --git a/tests/test_tls_rest_https.cpp b/tests/test_tls_rest_https.cpp index bf7974c..b669976 100644 --- a/tests/test_tls_rest_https.cpp +++ b/tests/test_tls_rest_https.cpp @@ -18,74 +18,72 @@ // A negative control points an HttpClient at a bogus CA and asserts the GET // fails, proving real certificate verification. -#include "tls_mocktest.hpp" -#include "signalwire/rest/rest_client.hpp" -#include "signalwire/rest/http_client.hpp" - #include +#include "signalwire/rest/http_client.hpp" +#include "signalwire/rest/rest_client.hpp" +#include "tls_mocktest.hpp" + // NOTE: these *_tls test files are #included into the single test_main.cpp // translation unit, so a file-scope `using namespace` would leak/collide with // other tests. In particular `RestClient` is ambiguous in this TU (the class // signalwire::rest::RestClient vs the top-level factory function // signalwire::RestClient in signalwire.hpp), so it is fully qualified below. namespace tt = signalwire::tlstest; -using signalwire::rest::HttpClient; using nlohmann::json; +using signalwire::rest::HttpClient; TEST(tls_rest_client_https_get) { - if (tt::ca_cert_path().empty() || !tt::rest_tls_available()) { - std::cerr << "(skipped: mock_signalwire --tls not reachable on " - << tt::rest_tls_base_url() << ") "; - return true; - } + if (tt::ca_cert_path().empty() || !tt::rest_tls_available()) { + std::cerr << "(skipped: mock_signalwire --tls not reachable on " << tt::rest_tls_base_url() + << ") "; + return true; + } - const std::string base = tt::rest_tls_base_url(); // https://127.0.0.1: + const std::string base = tt::rest_tls_base_url(); // https://127.0.0.1: - // ---- Path 1: explicit set_ca_cert_path() on a directly-built HttpClient. - // Clear SSL_CERT_FILE first so this proves the SETTER (not the env) carries - // the trust. - ::unsetenv("SSL_CERT_FILE"); - tt::rest_journal_reset(); - { - HttpClient http(base, "test_proj", "test_tok"); - http.set_ca_cert_path(tt::ca_cert_path()); - json body = http.get("/api/relay/rest/addresses", {{"page_size", "5"}}); - ASSERT_TRUE(body.is_object()); - ASSERT_TRUE(body.contains("data")); // real JSON over verified TLS - ASSERT_TRUE(body["data"].is_array()); + // ---- Path 1: explicit set_ca_cert_path() on a directly-built HttpClient. + // Clear SSL_CERT_FILE first so this proves the SETTER (not the env) carries + // the trust. + ::unsetenv("SSL_CERT_FILE"); + tt::rest_journal_reset(); + { + HttpClient http(base, "test_proj", "test_tok"); + http.set_ca_cert_path(tt::ca_cert_path()); + json body = http.get("/api/relay/rest/addresses", {{"page_size", "5"}}); + ASSERT_TRUE(body.is_object()); + ASSERT_TRUE(body.contains("data")); // real JSON over verified TLS + ASSERT_TRUE(body["data"].is_array()); - json last = tt::rest_journal_last(); // journal read over HTTPS too - ASSERT_EQ(last.value("method", std::string("")), std::string("GET")); - ASSERT_EQ(last.value("path", std::string("")), - std::string("/api/relay/rest/addresses")); - } + json last = tt::rest_journal_last(); // journal read over HTTPS too + ASSERT_EQ(last.value("method", std::string("")), std::string("GET")); + ASSERT_EQ(last.value("path", std::string("")), std::string("/api/relay/rest/addresses")); + } - // ---- Path 2: SSL_CERT_FILE env trust via the user-facing RestClient. - tt::trust_test_ca(); // SSL_CERT_FILE -> test CA - tt::rest_journal_reset(); - { - signalwire::rest::RestClient client = - signalwire::rest::RestClient::with_base_url(base, "test_proj", "test_tok"); - json body = client.addresses().list({{"page_size", "5"}}); - ASSERT_TRUE(body.is_object()); - ASSERT_TRUE(body.contains("data")); - ASSERT_TRUE(body["data"].is_array()); + // ---- Path 2: SSL_CERT_FILE env trust via the user-facing RestClient. + tt::trust_test_ca(); // SSL_CERT_FILE -> test CA + tt::rest_journal_reset(); + { + signalwire::rest::RestClient client = + signalwire::rest::RestClient::with_base_url(base, "test_proj", "test_tok"); + json body = client.addresses().list({{"page_size", "5"}}); + ASSERT_TRUE(body.is_object()); + ASSERT_TRUE(body.contains("data")); + ASSERT_TRUE(body["data"].is_array()); - json last = tt::rest_journal_last(); - ASSERT_EQ(last.value("path", std::string("")), - std::string("/api/relay/rest/addresses")); - } + json last = tt::rest_journal_last(); + ASSERT_EQ(last.value("path", std::string("")), std::string("/api/relay/rest/addresses")); + } - // ---- Negative control: a bogus CA must make verification fail. cpp-httplib - // raises SignalWireRestTransportError (a SignalWireRestError, status 0, - // "Connection failed") when the TLS handshake can't verify the server cert. - { - ::unsetenv("SSL_CERT_FILE"); - HttpClient http(base, "test_proj", "test_tok"); - http.set_ca_cert_path("/dev/null"); // empty/invalid trust store - ASSERT_THROWS((void)http.get("/api/relay/rest/addresses")); - tt::trust_test_ca(); // restore trust - } - return true; + // ---- Negative control: a bogus CA must make verification fail. cpp-httplib + // raises SignalWireRestTransportError (a SignalWireRestError, status 0, + // "Connection failed") when the TLS handshake can't verify the server cert. + { + ::unsetenv("SSL_CERT_FILE"); + HttpClient http(base, "test_proj", "test_tok"); + http.set_ca_cert_path("/dev/null"); // empty/invalid trust store + ASSERT_THROWS((void)http.get("/api/relay/rest/addresses")); + tt::trust_test_ca(); // restore trust + } + return true; } diff --git a/tests/test_tls_server_https.cpp b/tests/test_tls_server_https.cpp index e9f0974..6386fa7 100644 --- a/tests/test_tls_server_https.cpp +++ b/tests/test_tls_server_https.cpp @@ -18,20 +18,23 @@ // and asserts the handshake is rejected, proving the server's cert is actually // verified. -#include "tls_mocktest.hpp" -#include "signalwire/swml/service.hpp" -#include "httplib.h" +#include +#include +#include +#include #include +#include #include #include +#include +#include #include #include -#include -#include -#include -#include +#include "httplib.h" +#include "signalwire/swml/service.hpp" +#include "tls_mocktest.hpp" namespace tt = signalwire::tlstest; using nlohmann::json; @@ -42,99 +45,118 @@ namespace { // A small race window remains (the classic bind-0 pattern) but in a quiet test // container it is reliable; the poll-until-up loop below tolerates a slow bind. int pick_free_port() { - httplib::Server probe; // unused; we just need a socket helper - (void)probe; - int fd = ::socket(AF_INET, SOCK_STREAM, 0); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; - ::bind(fd, reinterpret_cast(&addr), sizeof(addr)); - socklen_t len = sizeof(addr); - ::getsockname(fd, reinterpret_cast(&addr), &len); - int port = ntohs(addr.sin_port); + httplib::Server probe; // unused; we just need a socket helper + (void)probe; + int fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + std::cerr << "pick_free_port: socket() failed: " << std::strerror(errno) << "\n"; + return -1; + } + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (::bind(fd, reinterpret_cast(&addr), sizeof(addr)) != 0) { + std::cerr << "pick_free_port: bind() failed: " << std::strerror(errno) << "\n"; + ::close(fd); + return -1; + } + socklen_t len = sizeof(addr); + if (::getsockname(fd, reinterpret_cast(&addr), &len) != 0) { + std::cerr << "pick_free_port: getsockname() failed: " << std::strerror(errno) << "\n"; ::close(fd); - return port; + return -1; + } + int port = ntohs(addr.sin_port); + ::close(fd); + if (port <= 0) { + std::cerr << "pick_free_port: kernel assigned no port\n"; + return -1; + } + return port; } -} // namespace +} // namespace TEST(tls_sdk_sslserver_verified_by_client) { - std::string ca = tt::ca_cert_path(); - if (ca.empty()) { - std::cerr << "(skipped: test CA not found) "; - return true; - } - // Locate the leaf cert/key alongside the CA (server.crt / server.key). - std::string certs_dir = ca.substr(0, ca.find_last_of('/')); - std::string cert = certs_dir + "/server.crt"; - std::string key = certs_dir + "/server.key"; - - int port = pick_free_port(); - - // Configure TLS exactly like the Python reference (env-driven). serve() - // reads these via resolve_tls_config_from_env() and builds an SSLServer. - ::setenv("SWML_SSL_ENABLED", "true", 1); - ::setenv("SWML_SSL_CERT_PATH", cert.c_str(), 1); - ::setenv("SWML_SSL_KEY_PATH", key.c_str(), 1); - ::unsetenv("PORT"); // don't let an ambient PORT override our chosen port - - signalwire::swml::Service svc; - svc.set_name("tls-cap-test").set_host("127.0.0.1").set_port(port); - - // serve() blocks (listen), so run it in a thread and stop it on the way out. - std::thread server_thread([&svc]() { svc.serve(); }); - - // Clean up TLS env + the server regardless of assertion outcome. - struct Guard { - signalwire::swml::Service& s; - std::thread& t; - ~Guard() { - s.stop(); - if (t.joinable()) t.join(); - ::unsetenv("SWML_SSL_ENABLED"); - ::unsetenv("SWML_SSL_CERT_PATH"); - ::unsetenv("SWML_SSL_KEY_PATH"); - } - } guard{svc, server_thread}; - - const std::string base = "https://127.0.0.1:" + std::to_string(port); - - // Poll /health over https:// (trusting the test CA) until the TLS listener - // is up, then assert a real verified response. - bool got_ok = false; - int got_status = 0; - std::string got_body; - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); - while (std::chrono::steady_clock::now() < deadline) { - httplib::Client cli(base); - cli.set_connection_timeout(1, 0); - cli.set_ca_cert_path(ca.c_str()); - cli.enable_server_certificate_verification(true); - auto res = cli.Get("/health"); - if (res && res->status == 200) { - got_ok = true; - got_status = res->status; - got_body = res->body; - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::string ca = tt::ca_cert_path(); + if (ca.empty()) { + std::cerr << "(skipped: test CA not found) "; + return true; + } + // Locate the leaf cert/key alongside the CA (server.crt / server.key). + std::string certs_dir = ca.substr(0, ca.find_last_of('/')); + std::string cert = certs_dir + "/server.crt"; + std::string key = certs_dir + "/server.key"; + + int port = pick_free_port(); + ASSERT_TRUE(port > 0); + + // Configure TLS exactly like the Python reference (env-driven). serve() + // reads these via resolve_tls_config_from_env() and builds an SSLServer. + ::setenv("SWML_SSL_ENABLED", "true", 1); + ::setenv("SWML_SSL_CERT_PATH", cert.c_str(), 1); + ::setenv("SWML_SSL_KEY_PATH", key.c_str(), 1); + ::unsetenv("PORT"); // don't let an ambient PORT override our chosen port + + signalwire::swml::Service svc; + svc.set_name("tls-cap-test").set_host("127.0.0.1").set_port(port); + + // serve() blocks (listen), so run it in a thread and stop it on the way out. + std::thread server_thread([&svc]() { svc.serve(); }); + + // Clean up TLS env + the server regardless of assertion outcome. + struct Guard { + signalwire::swml::Service& s; + std::thread& t; + ~Guard() { + s.stop(); + if (t.joinable()) { + t.join(); + } + ::unsetenv("SWML_SSL_ENABLED"); + ::unsetenv("SWML_SSL_CERT_PATH"); + ::unsetenv("SWML_SSL_KEY_PATH"); } - - ASSERT_TRUE(got_ok); // a verified TLS response came back - ASSERT_EQ(got_status, 200); - json payload = json::parse(got_body); - ASSERT_EQ(payload.value("status", std::string("")), std::string("healthy")); - - // Negative control: a client that does NOT trust the test CA must be - // rejected, proving the server presents a cert that is actually verified. - { - httplib::Client untrusted(base); - untrusted.set_connection_timeout(3, 0); - untrusted.set_ca_cert_path("/dev/null"); // empty trust store - untrusted.enable_server_certificate_verification(true); - auto neg = untrusted.Get("/health"); - ASSERT_FALSE(static_cast(neg)); // handshake/verify must fail + } guard{svc, server_thread}; + + const std::string base = "https://127.0.0.1:" + std::to_string(port); + + // Poll /health over https:// (trusting the test CA) until the TLS listener + // is up, then assert a real verified response. + bool got_ok = false; + int got_status = 0; + std::string got_body; + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (std::chrono::steady_clock::now() < deadline) { + httplib::Client cli(base); + cli.set_connection_timeout(1, 0); + cli.set_ca_cert_path(ca); + cli.enable_server_certificate_verification(true); + auto res = cli.Get("/health"); + if (res && res->status == 200) { + got_ok = true; + got_status = res->status; + got_body = res->body; + break; } - return true; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + ASSERT_TRUE(got_ok); // a verified TLS response came back + ASSERT_EQ(got_status, 200); + json payload = json::parse(got_body); + ASSERT_EQ(payload.value("status", std::string("")), std::string("healthy")); + + // Negative control: a client that does NOT trust the test CA must be + // rejected, proving the server presents a cert that is actually verified. + { + httplib::Client untrusted(base); + untrusted.set_connection_timeout(3, 0); + untrusted.set_ca_cert_path("/dev/null"); // empty trust store + untrusted.enable_server_certificate_verification(true); + auto neg = untrusted.Get("/health"); + ASSERT_FALSE(static_cast(neg)); // handshake/verify must fail + } + return true; } diff --git a/tests/test_tool.cpp b/tests/test_tool.cpp index 2b7d311..272b930 100644 --- a/tests/test_tool.cpp +++ b/tests/test_tool.cpp @@ -13,63 +13,62 @@ using json = nlohmann::json; // ======================================================================== TEST(tool_define_tool_basic) { - AgentBase agent; - agent.define_tool("greet", "Say hello", json::object(), - [](const json&, const json&) -> FunctionResult { - return FunctionResult("Hello!"); - }); - ASSERT_TRUE(agent.has_tool("greet")); - return true; + AgentBase agent; + agent.define_tool( + "greet", "Say hello", json::object(), + [](const json&, const json&) -> FunctionResult { return FunctionResult("Hello!"); }); + ASSERT_TRUE(agent.has_tool("greet")); + return true; } TEST(tool_define_tool_with_definition) { - AgentBase agent; - ToolDefinition td; - td.name = "my_tool"; - td.description = "A tool"; - td.parameters = json::object({{"type", "object"}, {"properties", json::object()}}); - td.handler = [](const json&, const json&) { return FunctionResult("ok"); }; - agent.define_tool(td); - ASSERT_TRUE(agent.has_tool("my_tool")); - return true; + AgentBase agent; + ToolDefinition td; + td.name = "my_tool"; + td.description = "A tool"; + td.parameters = json::object({{"type", "object"}, {"properties", json::object()}}); + td.handler = [](const json&, const json&) { return FunctionResult("ok"); }; + agent.define_tool(td); + ASSERT_TRUE(agent.has_tool("my_tool")); + return true; } TEST(tool_has_tool_false_for_unknown) { - AgentBase agent; - ASSERT_FALSE(agent.has_tool("does_not_exist")); - return true; + AgentBase agent; + ASSERT_FALSE(agent.has_tool("does_not_exist")); + return true; } TEST(tool_list_tools_empty) { - AgentBase agent; - auto tools = agent.list_tools(); - ASSERT_EQ(tools.size(), 0u); - return true; + AgentBase agent; + auto tools = agent.list_tools(); + ASSERT_EQ(tools.size(), 0u); + return true; } TEST(tool_list_tools_preserves_order) { - AgentBase agent; - agent.define_tool("charlie", "C", json::object(), nullptr); - agent.define_tool("alpha", "A", json::object(), nullptr); - agent.define_tool("bravo", "B", json::object(), nullptr); - auto tools = agent.list_tools(); - ASSERT_EQ(tools.size(), 3u); - ASSERT_EQ(tools[0], "charlie"); - ASSERT_EQ(tools[1], "alpha"); - ASSERT_EQ(tools[2], "bravo"); - return true; + AgentBase agent; + agent.define_tool("charlie", "C", json::object(), nullptr); + agent.define_tool("alpha", "A", json::object(), nullptr); + agent.define_tool("bravo", "B", json::object(), nullptr); + auto tools = agent.list_tools(); + ASSERT_EQ(tools.size(), 3u); + ASSERT_EQ(tools[0], "charlie"); + ASSERT_EQ(tools[1], "alpha"); + ASSERT_EQ(tools[2], "bravo"); + return true; } TEST(tool_redefine_does_not_duplicate_order) { - AgentBase agent; - agent.define_tool("tool_a", "A", json::object(), nullptr); - agent.define_tool("tool_b", "B", json::object(), nullptr); - agent.define_tool("tool_a", "A updated", json::object(), nullptr); - auto tools = agent.list_tools(); - ASSERT_EQ(tools.size(), 2u); - ASSERT_EQ(tools[0], "tool_a"); - ASSERT_EQ(tools[1], "tool_b"); - return true; + AgentBase agent; + agent.define_tool("tool_a", "A", json::object(), nullptr); + agent.define_tool("tool_b", "B", json::object(), nullptr); + agent.define_tool("tool_a", "A updated", json::object(), nullptr); + auto tools = agent.list_tools(); + ASSERT_EQ(tools.size(), 2u); + ASSERT_EQ(tools[0], "tool_a"); + ASSERT_EQ(tools[1], "tool_b"); + return true; } // ======================================================================== @@ -77,46 +76,45 @@ TEST(tool_redefine_does_not_duplicate_order) { // ======================================================================== TEST(tool_dispatch_returns_result) { - AgentBase agent; - agent.define_tool("add", "Add numbers", json::object(), - [](const json& args, const json&) -> FunctionResult { - int a = args.value("a", 0); - int b = args.value("b", 0); - return FunctionResult("Result: " + std::to_string(a + b)); - }); - auto result = agent.on_function_call("add", - json::object({{"a", 3}, {"b", 4}}), json::object()); - ASSERT_EQ(result.to_json()["response"].get(), "Result: 7"); - return true; + AgentBase agent; + agent.define_tool("add", "Add numbers", json::object(), + [](const json& args, const json&) -> FunctionResult { + int a = args.value("a", 0); + int b = args.value("b", 0); + return FunctionResult("Result: " + std::to_string(a + b)); + }); + auto result = agent.on_function_call("add", json::object({{"a", 3}, {"b", 4}}), json::object()); + ASSERT_EQ(result.to_json()["response"].get(), "Result: 7"); + return true; } TEST(tool_dispatch_unknown_function) { - AgentBase agent; - auto result = agent.on_function_call("missing", json::object(), json::object()); - auto j = result.to_json(); - ASSERT_TRUE(j["response"].get().find("Unknown") != std::string::npos); - return true; + AgentBase agent; + auto result = agent.on_function_call("missing", json::object(), json::object()); + auto j = result.to_json(); + ASSERT_TRUE(j["response"].get().find("Unknown") != std::string::npos); + return true; } TEST(tool_dispatch_null_handler) { - AgentBase agent; - agent.define_tool("null_handler", "No handler", json::object(), nullptr); - auto result = agent.on_function_call("null_handler", json::object(), json::object()); - auto j = result.to_json(); - ASSERT_TRUE(j["response"].get().find("No handler") != std::string::npos); - return true; + AgentBase agent; + agent.define_tool("null_handler", "No handler", json::object(), nullptr); + auto result = agent.on_function_call("null_handler", json::object(), json::object()); + auto j = result.to_json(); + ASSERT_TRUE(j["response"].get().find("No handler") != std::string::npos); + return true; } TEST(tool_dispatch_with_raw_data) { - AgentBase agent; - agent.define_tool("echo_raw", "Echo raw", json::object(), - [](const json&, const json& raw) -> FunctionResult { - return FunctionResult("call_id=" + raw.value("call_id", "none")); - }); - auto result = agent.on_function_call("echo_raw", json::object(), - json::object({{"call_id", "call-123"}})); - ASSERT_EQ(result.to_json()["response"].get(), "call_id=call-123"); - return true; + AgentBase agent; + agent.define_tool("echo_raw", "Echo raw", json::object(), + [](const json&, const json& raw) -> FunctionResult { + return FunctionResult("call_id=" + raw.value("call_id", "none")); + }); + auto result = + agent.on_function_call("echo_raw", json::object(), json::object({{"call_id", "call-123"}})); + ASSERT_EQ(result.to_json()["response"].get(), "call_id=call-123"); + return true; } // ======================================================================== @@ -124,34 +122,34 @@ TEST(tool_dispatch_with_raw_data) { // ======================================================================== TEST(tool_register_datamap_function) { - AgentBase agent; - agent.set_auth("u", "p"); - auto dm = DataMap("get_weather") - .purpose("Get weather") - .parameter("city", "string", "City", true) - .webhook("GET", "https://api.example.com/weather") - .output(FunctionResult("Weather: ${response.temp}")) - .to_swaig_function(); - agent.register_swaig_function(dm); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto& funcs = verb["ai"]["SWAIG"]["functions"]; - bool found = false; - for (const auto& f : funcs) { - if (f.value("function", "") == "get_weather") { - found = true; - ASSERT_TRUE(f.contains("data_map")); - } - } - ASSERT_TRUE(found); - return true; + AgentBase agent; + agent.set_auth("u", "p"); + auto dm = DataMap("get_weather") + .purpose("Get weather") + .parameter("city", "string", "City", true) + .webhook("GET", "https://api.example.com/weather") + .output(FunctionResult("Weather: ${response.temp}")) + .to_swaig_function(); + agent.register_swaig_function(dm); + + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto& funcs = verb["ai"]["SWAIG"]["functions"]; + bool found = false; + for (const auto& f : funcs) { + if (f.value("function", "") == "get_weather") { + found = true; + ASSERT_TRUE(f.contains("data_map")); } + } + ASSERT_TRUE(found); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -159,43 +157,46 @@ TEST(tool_register_datamap_function) { // ======================================================================== TEST(tool_swaig_functions_in_swml) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.define_tool("search", "Search web", json::object({ - {"type", "object"}, - {"properties", json::object({ - {"q", json::object({{"type", "string"}})} - })} - }), [](const json&, const json&) { return FunctionResult("ok"); }); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto& funcs = verb["ai"]["SWAIG"]["functions"]; - ASSERT_EQ(funcs.size(), 1u); - ASSERT_EQ(funcs[0]["function"].get(), "search"); - ASSERT_TRUE(funcs[0].contains("web_hook_url")); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool( + "search", "Search web", + json::object({{"type", "object"}, + {"properties", json::object({{"q", json::object({{"type", "string"}})}})}}), + [](const json&, const json&) { return FunctionResult("ok"); }); + + // Render WITH a call_id: a per-tool web_hook_url is only emitted when the + // entry carries a token (or SWAIG query params) — reference + // agent_base.py:1085-1099. + const std::map query = {{"call_id", "call-abc"}}; + json swml = agent.render_swml_for_request(query, json::object(), {}); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto& funcs = verb["ai"]["SWAIG"]["functions"]; + ASSERT_EQ(funcs.size(), 1u); + ASSERT_EQ(funcs[0]["function"].get(), "search"); + ASSERT_TRUE(funcs[0].contains("web_hook_url")); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // Locate the single rendered SWAIG function entry in a rendered SWML document. static json swaig_only_function(const json& swml) { - const auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG") && - verb["ai"]["SWAIG"].contains("functions")) { - const auto& funcs = verb["ai"]["SWAIG"]["functions"]; - if (funcs.size() == 1u) { - return funcs[0]; - } - } + const auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG") && + verb["ai"]["SWAIG"].contains("functions")) { + const auto& funcs = verb["ai"]["SWAIG"]["functions"]; + if (funcs.size() == 1u) { + return funcs[0]; + } } - return json(); + } + return json(); } // A tool defined WITHOUT an explicit secure argument is SECURE (the A1 default), @@ -203,94 +204,195 @@ static json swaig_only_function(const json& swml) { // the wire manifestation of secure. ``secure`` itself is never emitted as a // function property (not in the SWML schema, not in the reference). TEST(tool_secure_tool_in_swml_carries_token) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.define_tool("secure_tool", "Secure", json::object(), - [](const json&, const json&) { return FunctionResult("ok"); }); - - const std::map query = {{"call_id", "call-abc"}}; - json fn = swaig_only_function(agent.render_swml_for_request(query, json::object(), {})); - ASSERT_FALSE(fn.is_null()); - ASSERT_FALSE(fn.contains("secure")); - ASSERT_TRUE(fn.contains("web_hook_url")); - ASSERT_TRUE(fn["web_hook_url"].get().find("__token=") != std::string::npos); - return true; + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool("secure_tool", "Secure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }); + + const std::map query = {{"call_id", "call-abc"}}; + json fn = swaig_only_function(agent.render_swml_for_request(query, json::object(), {})); + ASSERT_FALSE(fn.is_null()); + ASSERT_FALSE(fn.contains("secure")); + ASSERT_TRUE(fn.contains("web_hook_url")); + ASSERT_TRUE(fn["web_hook_url"].get().find("__token=") != std::string::npos); + return true; } -// The other direction: an explicitly INSECURE tool gets NO token, so a port -// cannot blindly tokenize every function. -TEST(tool_insecure_tool_in_swml_has_no_token) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.define_tool("open_tool", "Insecure", json::object(), - [](const json&, const json&) { return FunctionResult("ok"); }, - false /* secure */); - - const std::map query = {{"call_id", "call-abc"}}; - json fn = swaig_only_function(agent.render_swml_for_request(query, json::object(), {})); - ASSERT_FALSE(fn.is_null()); - ASSERT_TRUE(fn.contains("web_hook_url")); - ASSERT_TRUE(fn["web_hook_url"].get().find("__token=") == std::string::npos); - return true; +// Locate a rendered SWAIG function entry BY NAME (the multi-tool analog of +// swaig_only_function). Returns a null json when the name is absent. +static json swaig_function_named(const json& swml, const std::string& name) { + const auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG") && + verb["ai"]["SWAIG"].contains("functions")) { + for (const auto& fn : verb["ai"]["SWAIG"]["functions"]) { + if (fn.contains("function") && fn["function"] == name) { + return fn; + } + } + } + } + return json(); } -// No call_id = no call to scope a token to, so even a secure tool renders bare -// (mirrors the reference's ``if func.secure and call_id`` guard). -TEST(tool_secure_tool_without_call_id_has_no_token) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.define_tool("secure_tool", "Secure", json::object(), - [](const json&, const json&) { return FunctionResult("ok"); }); - - json fn = swaig_only_function(agent.render_swml()); - ASSERT_FALSE(fn.is_null()); - ASSERT_TRUE(fn.contains("web_hook_url")); - ASSERT_TRUE(fn["web_hook_url"].get().find("__token=") == std::string::npos); - return true; +// The other direction, and the SECURITY half of the contract: an explicitly +// INSECURE tool gets no token AND NO ``web_hook_url`` KEY AT ALL. +// +// Reference agent_base.py:1085-1099 — external URL wins; else a local URL ONLY +// when a token or SWAIG query params exist; else the key is absent and the tool +// falls back to the shared ``SWAIG.defaults.web_hook_url``. Emitting the local +// URL here would publish an UNAUTHENTICATED, function-specific callback on the +// wire, which is exactly what ``secure=false`` must not do. An empty string, a +// null, or a tokenless URL are the same defect — the KEY must be absent. +TEST(tool_insecure_tool_in_swml_has_no_webhook_key) { + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool( + "open_tool", "Insecure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }, false /* secure */); + + const std::map query = {{"call_id", "call-abc"}}; + json fn = swaig_only_function(agent.render_swml_for_request(query, json::object(), {})); + ASSERT_FALSE(fn.is_null()); + ASSERT_FALSE(fn.contains("web_hook_url")); + return true; +} + +// SWAIG query params are the OTHER arm of the reference's ``elif token or +// agent._swaig_query_params`` guard: with them set, even an insecure tool gets +// its own (still tokenless) local webhook, because the params must reach the +// callback. This pins that the guard is the reference's disjunction and not a +// blanket "insecure => no webhook". +TEST(tool_insecure_tool_with_swaig_query_params_keeps_webhook) { + AgentBase agent; + agent.set_auth("u", "p"); + agent.add_swaig_query_param("tenant", "acme"); + agent.define_tool( + "open_tool", "Insecure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }, false /* secure */); + + const std::map query = {{"call_id", "call-abc"}}; + json fn = swaig_only_function(agent.render_swml_for_request(query, json::object(), {})); + ASSERT_FALSE(fn.is_null()); + ASSERT_TRUE(fn.contains("web_hook_url")); + const std::string url = fn["web_hook_url"].get(); + ASSERT_TRUE(url.find("tenant=acme") != std::string::npos); + ASSERT_TRUE(url.find("__token=") == std::string::npos); + return true; +} + +// The SECURE-DEFAULT corpus shape, in-process: one default (secure) tool and one +// secure=false tool on the SAME agent, rendered in ONE pass. The secure entry +// HAS a web_hook_url carrying ``__token``; the insecure entry has NO +// web_hook_url key at all. This is the pair the cross-port +// diff_port_secure_default gate compares. +TEST(tool_secure_and_insecure_tools_render_divergent_webhooks) { + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool("sd_default_secure", "Secure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }); + agent.define_tool( + "sd_explicit_insecure", "Insecure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }, false /* secure */); + + const std::map query = {{"call_id", "call-abc"}}; + const json swml = agent.render_swml_for_request(query, json::object(), {}); + + json secure_fn = swaig_function_named(swml, "sd_default_secure"); + ASSERT_FALSE(secure_fn.is_null()); + ASSERT_TRUE(secure_fn.contains("web_hook_url")); + ASSERT_TRUE(secure_fn["web_hook_url"].get().find("__token=") != std::string::npos); + + json insecure_fn = swaig_function_named(swml, "sd_explicit_insecure"); + ASSERT_FALSE(insecure_fn.is_null()); + ASSERT_FALSE(insecure_fn.contains("web_hook_url")); + + // ...and the shared fallback the insecure tool relies on MUST be present. + // Withholding the per-tool webhook without emitting SWAIG.defaults leaves an + // insecure tool with NO reachable callback at all — a worse failure than the + // unauthenticated per-tool callback the guard removes, and one the + // cross-port SECURE-DEFAULT gate cannot see (it inspects only functions[]). + // Reference agent_base.py:1108-1113 adds defaults whenever functions exist. + json swaig; + for (const auto& verb : swml["sections"]["main"]) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + swaig = verb["ai"]["SWAIG"]; + } + } + ASSERT_FALSE(swaig.is_null()); + ASSERT_TRUE(swaig.contains("defaults")); + ASSERT_TRUE(swaig["defaults"].contains("web_hook_url")); + const std::string fallback = swaig["defaults"]["web_hook_url"].get(); + ASSERT_TRUE(fallback.find("/swaig") != std::string::npos); + // The shared endpoint is not per-tool, so it carries no per-tool token. + ASSERT_TRUE(fallback.find("__token=") == std::string::npos); + return true; +} + +// No call_id = no call to scope a token to under C++'s ``if secure && call_id`` +// guard, so no token is minted — and with no token and no SWAIG query params the +// reference's ``elif token or _swaig_query_params`` is false on both arms, so no +// ``web_hook_url`` key is emitted either. +// +// NOTE (measured, not inferred; out of scope for this security fix): the python +// reference GENERATES a call_id when the request supplies none +// (agent_base.py ``generated_call_id``), so a secure tool there always mints a +// token and always carries its own webhook. C++ renders bare instead. That is a +// separate divergence in WHEN a call_id exists, not in this webhook-key guard, +// and the SECURE-DEFAULT corpus always passes an explicit call_id so it does not +// exercise it. This test pins C++'s current no-call_id behavior. +TEST(tool_secure_tool_without_call_id_has_no_webhook_key) { + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool("secure_tool", "Secure", json::object(), + [](const json&, const json&) { return FunctionResult("ok"); }); + + json fn = swaig_only_function(agent.render_swml()); + ASSERT_FALSE(fn.is_null()); + ASSERT_FALSE(fn.contains("web_hook_url")); + return true; } TEST(tool_function_includes) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.add_function_include(json::object({ - {"url", "https://example.com/functions.json"}, - {"functions", json::array({"func1", "func2"})} - })); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - ASSERT_TRUE(verb["ai"]["SWAIG"].contains("includes")); - ASSERT_EQ(verb["ai"]["SWAIG"]["includes"].size(), 1u); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.add_function_include(json::object({{"url", "https://example.com/functions.json"}, + {"functions", json::array({"func1", "func2"})}})); + + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + ASSERT_TRUE(verb["ai"]["SWAIG"].contains("includes")); + ASSERT_EQ(verb["ai"]["SWAIG"]["includes"].size(), 1u); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } TEST(tool_set_function_includes_replaces) { - AgentBase agent; - agent.set_auth("u", "p"); - // set_function_includes REPLACES the prior add_function_include. Use - // well-formed entries (non-empty string `url` + array `functions`) so the - // #191 validity filter keeps them; this test isolates replace-vs-merge. - agent.add_function_include( - json::object({{"url", "https://a/swaig"}, {"functions", json::array({"a"})}})); - agent.set_function_includes({ - json::object({{"url", "https://b/swaig"}, {"functions", json::array({"b"})}}), - json::object({{"url", "https://c/swaig"}, {"functions", json::array({"c"})}}), - }); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - ASSERT_EQ(verb["ai"]["SWAIG"]["includes"].size(), 2u); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + // set_function_includes REPLACES the prior add_function_include. Use + // well-formed entries (non-empty string `url` + array `functions`) so the + // #191 validity filter keeps them; this test isolates replace-vs-merge. + agent.add_function_include( + json::object({{"url", "https://a/swaig"}, {"functions", json::array({"a"})}})); + agent.set_function_includes({ + json::object({{"url", "https://b/swaig"}, {"functions", json::array({"b"})}}), + json::object({{"url", "https://c/swaig"}, {"functions", json::array({"c"})}}), + }); + + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + ASSERT_EQ(verb["ai"]["SWAIG"]["includes"].size(), 2u); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } diff --git a/tests/test_tts_gender_enum.cpp b/tests/test_tts_gender_enum.cpp index 23f3804..bfd42dd 100644 --- a/tests/test_tts_gender_enum.cpp +++ b/tests/test_tts_gender_enum.cpp @@ -21,12 +21,12 @@ using json = nlohmann::json; // media params' `gender` key (the documented say_gender values: male/female). // This is the single normalization point shared by the typed overloads. TEST(tts_gender_enum_maps_to_wire_string) { - ASSERT_EQ(tts_gender_value(Gender::Male), std::string("male")); - ASSERT_EQ(tts_gender_value(Gender::Female), std::string("female")); - // ADL to_string() agrees with tts_gender_value(). - ASSERT_EQ(to_string(Gender::Female), std::string("female")); - ASSERT_EQ(to_string(Gender::Male), std::string("male")); - return true; + ASSERT_EQ(tts_gender_value(Gender::Male), std::string("male")); + ASSERT_EQ(tts_gender_value(Gender::Female), std::string("female")); + // ADL to_string() agrees with tts_gender_value(). + ASSERT_EQ(to_string(Gender::Female), std::string("female")); + ASSERT_EQ(to_string(Gender::Male), std::string("male")); + return true; } // Build the TTS media frame exactly the way Call::play_tts does, once seeded @@ -34,26 +34,28 @@ TEST(tts_gender_enum_maps_to_wire_string) { // once from the literal string. The two frames must be byte-identical — // proving the enum path and the string path emit the IDENTICAL on-wire shape. static json build_tts_media(const std::string& text, const std::string& gender) { - json tts; - tts["text"] = text; - if (!gender.empty()) tts["gender"] = gender; - return json::array({ {{"type", "tts"}, {"params", tts}} }); + json tts; + tts["text"] = text; + if (!gender.empty()) { + tts["gender"] = gender; + } + return json::array({{{"type", "tts"}, {"params", tts}}}); } TEST(tts_gender_enum_and_string_build_identical_frame) { - // play_tts(Gender::Female, ...) routes the wire string tts_gender_value() - // returns into the same builder the std::string overload uses. - json from_enum = build_tts_media("hello", tts_gender_value(Gender::Female)); - json from_string = build_tts_media("hello", "female"); - ASSERT_EQ(from_enum, from_string); - ASSERT_EQ(from_enum[0]["params"].value("gender", ""), std::string("female")); + // play_tts(Gender::Female, ...) routes the wire string tts_gender_value() + // returns into the same builder the std::string overload uses. + json from_enum = build_tts_media("hello", tts_gender_value(Gender::Female)); + json from_string = build_tts_media("hello", "female"); + ASSERT_EQ(from_enum, from_string); + ASSERT_EQ(from_enum[0]["params"].value("gender", ""), std::string("female")); - // Male maps identically. - json male_enum = build_tts_media("hi", tts_gender_value(Gender::Male)); - json male_string = build_tts_media("hi", "male"); - ASSERT_EQ(male_enum, male_string); - ASSERT_EQ(male_enum[0]["params"].value("gender", ""), std::string("male")); - return true; + // Male maps identically. + json male_enum = build_tts_media("hi", tts_gender_value(Gender::Male)); + json male_string = build_tts_media("hi", "male"); + ASSERT_EQ(male_enum, male_string); + ASSERT_EQ(male_enum[0]["params"].value("gender", ""), std::string("male")); + return true; } // The typed overloads exist and are callable on a real Call (no client: the @@ -61,17 +63,17 @@ TEST(tts_gender_enum_and_string_build_identical_frame) { // the enum signature is proven to compile + dispatch against the live API). // The string overload stays the canonical signature; the enum is additive. TEST(tts_gender_enum_overloads_callable_on_call) { - Call call("c-gender", "n-gender"); - // play_tts(text, language, Gender, ...) resolves to the typed overload. - Action a = call.play_tts("hi", "en-US", Gender::Female); - ASSERT_TRUE(a.completed()); // no client -> resolves immediately - // prompt_tts(text, collect, language, Gender, ...) likewise. - json collect; - collect["digits"]["max"] = 1; - Action b = call.prompt_tts("press one", collect, "en-US", Gender::Male); - ASSERT_TRUE(b.completed()); - // The bare-string overload still resolves the same way (parity / open set). - Action c = call.play_tts("hi", "en-US", "neutral"); // engine value, str path - ASSERT_TRUE(c.completed()); - return true; + Call call("c-gender", "n-gender"); + // play_tts(text, language, Gender, ...) resolves to the typed overload. + Action a = call.play_tts("hi", "en-US", Gender::Female); + ASSERT_TRUE(a.completed()); // no client -> resolves immediately + // prompt_tts(text, collect, language, Gender, ...) likewise. + json collect; + collect["digits"]["max"] = 1; + Action b = call.prompt_tts("press one", collect, "en-US", Gender::Male); + ASSERT_TRUE(b.completed()); + // The bare-string overload still resolves the same way (parity / open set). + Action c = call.play_tts("hi", "en-US", "neutral"); // engine value, str path + ASSERT_TRUE(c.completed()); + return true; } diff --git a/tests/test_type_inference.cpp b/tests/test_type_inference.cpp index 016add1..08b3df0 100644 --- a/tests/test_type_inference.cpp +++ b/tests/test_type_inference.cpp @@ -23,8 +23,7 @@ TEST(infer_schema_from_typed_builder) { .integer("count", "How many") .required({"service"}); - auto [parameters, required, description, is_typed, has_raw_data] = - sw_ti::infer_schema(schema); + auto [parameters, required, description, is_typed, has_raw_data] = sw_ti::infer_schema(schema); // parameters is the properties map (name -> property). ASSERT_TRUE(parameters.is_object()); @@ -52,8 +51,9 @@ TEST(infer_schema_description_passthrough) { schema.string("q"); auto result = sw_ti::infer_schema(schema, std::optional("Book a service")); - ASSERT_TRUE(std::get<2>(result).has_value()); - ASSERT_EQ(*std::get<2>(result), std::string("Book a service")); + auto& t = std::get<2>(result); + ASSERT_TRUE(t.has_value()); + ASSERT_EQ(*t, std::string("Book a service")); ASSERT_TRUE(std::get<3>(result)); // is_typed return true; } @@ -64,8 +64,7 @@ TEST(infer_schema_raw_data_excluded_but_flagged) { sw_swaig::ParameterSchema schema; schema.string("name").string("raw_data").required({"name", "raw_data"}); - auto [parameters, required, description, is_typed, has_raw_data] = - sw_ti::infer_schema(schema); + auto [parameters, required, description, is_typed, has_raw_data] = sw_ti::infer_schema(schema); ASSERT_TRUE(parameters.contains("name")); ASSERT_FALSE(parameters.contains("raw_data")); @@ -79,8 +78,7 @@ TEST(infer_schema_raw_data_excluded_but_flagged) { TEST(infer_schema_empty_builder_is_untyped) { sw_swaig::ParameterSchema schema; // no properties declared - auto [parameters, required, description, is_typed, has_raw_data] = - sw_ti::infer_schema(schema); + auto [parameters, required, description, is_typed, has_raw_data] = sw_ti::infer_schema(schema); ASSERT_TRUE(parameters.empty()); ASSERT_TRUE(required.empty()); ASSERT_FALSE(is_typed); diff --git a/tests/test_url_validator.cpp b/tests/test_url_validator.cpp index ca8f78b..a6924b1 100644 --- a/tests/test_url_validator.cpp +++ b/tests/test_url_validator.cpp @@ -3,32 +3,35 @@ // The DNS resolver is stubbed via _set_resolver() so the suite stays // hermetic. -#include "signalwire/utils/url_validator.hpp" - #include #include #include #include +#include "signalwire/utils/url_validator.hpp" + using namespace signalwire::utils::url_validator; namespace { void stub_resolver(const std::string& ip) { - _set_resolver([ip](const std::string&) -> std::optional> { - return std::vector{ip}; - }); + // A test stub for the DNS resolver hook. The only thing that can throw here + // is the vector allocation, i.e. bad_alloc, which is not a condition this + // stub can meaningfully handle or that the test is exercising. + // NOLINTNEXTLINE(bugprone-exception-escape) + _set_resolver([ip](const std::string&) -> std::optional> { + return std::vector{ip}; + }); } void stub_failed_resolver() { - _set_resolver([](const std::string&) -> std::optional> { - return std::nullopt; - }); + _set_resolver( + [](const std::string&) -> std::optional> { return std::nullopt; }); } void reset_state() { - _set_resolver(nullptr); - ::unsetenv("SWML_ALLOW_PRIVATE_URLS"); + _set_resolver(nullptr); + ::unsetenv("SWML_ALLOW_PRIVATE_URLS"); } } // namespace @@ -36,180 +39,180 @@ void reset_state() { // --- Scheme ---------------------------------------------------------- TEST(url_validator_http_scheme_allowed) { - reset_state(); - stub_resolver("1.2.3.4"); - ASSERT_TRUE(validate_url("http://example.com")); - reset_state(); - return true; + reset_state(); + stub_resolver("1.2.3.4"); + ASSERT_TRUE(validate_url("http://example.com")); + reset_state(); + return true; } TEST(url_validator_https_scheme_allowed) { - reset_state(); - stub_resolver("1.2.3.4"); - ASSERT_TRUE(validate_url("https://example.com")); - reset_state(); - return true; + reset_state(); + stub_resolver("1.2.3.4"); + ASSERT_TRUE(validate_url("https://example.com")); + reset_state(); + return true; } TEST(url_validator_ftp_scheme_rejected) { - reset_state(); - ASSERT_FALSE(validate_url("ftp://example.com")); - return true; + reset_state(); + ASSERT_FALSE(validate_url("ftp://example.com")); + return true; } TEST(url_validator_file_scheme_rejected) { - reset_state(); - ASSERT_FALSE(validate_url("file:///etc/passwd")); - return true; + reset_state(); + ASSERT_FALSE(validate_url("file:///etc/passwd")); + return true; } TEST(url_validator_javascript_scheme_rejected) { - reset_state(); - ASSERT_FALSE(validate_url("javascript:alert(1)")); - return true; + reset_state(); + ASSERT_FALSE(validate_url("javascript:alert(1)")); + return true; } // --- Hostname -------------------------------------------------------- TEST(url_validator_no_hostname_rejected) { - reset_state(); - ASSERT_FALSE(validate_url("http://")); - return true; + reset_state(); + ASSERT_FALSE(validate_url("http://")); + return true; } TEST(url_validator_unresolvable_hostname_rejected) { - reset_state(); - stub_failed_resolver(); - ASSERT_FALSE(validate_url("http://nonexistent.invalid")); - reset_state(); - return true; + reset_state(); + stub_failed_resolver(); + ASSERT_FALSE(validate_url("http://nonexistent.invalid")); + reset_state(); + return true; } // --- Blocked ranges ------------------------------------------------- TEST(url_validator_loopback_ipv4_rejected) { - reset_state(); - stub_resolver("127.0.0.1"); - ASSERT_FALSE(validate_url("http://localhost")); - reset_state(); - return true; + reset_state(); + stub_resolver("127.0.0.1"); + ASSERT_FALSE(validate_url("http://localhost")); + reset_state(); + return true; } TEST(url_validator_rfc1918_10_rejected) { - reset_state(); - stub_resolver("10.0.0.5"); - ASSERT_FALSE(validate_url("http://internal")); - reset_state(); - return true; + reset_state(); + stub_resolver("10.0.0.5"); + ASSERT_FALSE(validate_url("http://internal")); + reset_state(); + return true; } TEST(url_validator_rfc1918_192_rejected) { - reset_state(); - stub_resolver("192.168.1.1"); - ASSERT_FALSE(validate_url("http://router")); - reset_state(); - return true; + reset_state(); + stub_resolver("192.168.1.1"); + ASSERT_FALSE(validate_url("http://router")); + reset_state(); + return true; } TEST(url_validator_rfc1918_172_rejected) { - reset_state(); - stub_resolver("172.16.0.1"); - ASSERT_FALSE(validate_url("http://corp")); - reset_state(); - return true; + reset_state(); + stub_resolver("172.16.0.1"); + ASSERT_FALSE(validate_url("http://corp")); + reset_state(); + return true; } TEST(url_validator_link_local_metadata_rejected) { - reset_state(); - stub_resolver("169.254.169.254"); - ASSERT_FALSE(validate_url("http://metadata")); - reset_state(); - return true; + reset_state(); + stub_resolver("169.254.169.254"); + ASSERT_FALSE(validate_url("http://metadata")); + reset_state(); + return true; } TEST(url_validator_zero_ip_rejected) { - reset_state(); - stub_resolver("0.0.0.0"); - ASSERT_FALSE(validate_url("http://void")); - reset_state(); - return true; + reset_state(); + stub_resolver("0.0.0.0"); + ASSERT_FALSE(validate_url("http://void")); + reset_state(); + return true; } TEST(url_validator_ipv6_loopback_rejected) { - reset_state(); - stub_resolver("::1"); - ASSERT_FALSE(validate_url("http://[::1]")); - reset_state(); - return true; + reset_state(); + stub_resolver("::1"); + ASSERT_FALSE(validate_url("http://[::1]")); + reset_state(); + return true; } TEST(url_validator_ipv6_link_local_rejected) { - reset_state(); - stub_resolver("fe80::1"); - ASSERT_FALSE(validate_url("http://link-local")); - reset_state(); - return true; + reset_state(); + stub_resolver("fe80::1"); + ASSERT_FALSE(validate_url("http://link-local")); + reset_state(); + return true; } TEST(url_validator_ipv6_private_rejected) { - reset_state(); - stub_resolver("fc00::1"); - ASSERT_FALSE(validate_url("http://ipv6-private")); - reset_state(); - return true; + reset_state(); + stub_resolver("fc00::1"); + ASSERT_FALSE(validate_url("http://ipv6-private")); + reset_state(); + return true; } TEST(url_validator_public_ip_allowed) { - reset_state(); - stub_resolver("8.8.8.8"); - ASSERT_TRUE(validate_url("http://dns.google")); - reset_state(); - return true; + reset_state(); + stub_resolver("8.8.8.8"); + ASSERT_TRUE(validate_url("http://dns.google")); + reset_state(); + return true; } // --- allow_private bypass ------------------------------------------ TEST(url_validator_allow_private_param_bypasses_check) { - reset_state(); - // No resolver stub: bypass short-circuits before DNS. - ASSERT_TRUE(validate_url("http://10.0.0.5", true)); - return true; + reset_state(); + // No resolver stub: bypass short-circuits before DNS. + ASSERT_TRUE(validate_url("http://10.0.0.5", true)); + return true; } TEST(url_validator_env_var_bypasses_check) { - reset_state(); - ::setenv("SWML_ALLOW_PRIVATE_URLS", "true", 1); - ASSERT_TRUE(validate_url("http://10.0.0.5")); - reset_state(); - return true; + reset_state(); + ::setenv("SWML_ALLOW_PRIVATE_URLS", "true", 1); + ASSERT_TRUE(validate_url("http://10.0.0.5")); + reset_state(); + return true; } TEST(url_validator_env_var_yes_bypasses_check) { - reset_state(); - ::setenv("SWML_ALLOW_PRIVATE_URLS", "YES", 1); - ASSERT_TRUE(validate_url("http://10.0.0.5")); - reset_state(); - return true; + reset_state(); + ::setenv("SWML_ALLOW_PRIVATE_URLS", "YES", 1); + ASSERT_TRUE(validate_url("http://10.0.0.5")); + reset_state(); + return true; } TEST(url_validator_env_var_1_bypasses_check) { - reset_state(); - ::setenv("SWML_ALLOW_PRIVATE_URLS", "1", 1); - ASSERT_TRUE(validate_url("http://10.0.0.5")); - reset_state(); - return true; + reset_state(); + ::setenv("SWML_ALLOW_PRIVATE_URLS", "1", 1); + ASSERT_TRUE(validate_url("http://10.0.0.5")); + reset_state(); + return true; } TEST(url_validator_env_var_false_does_not_bypass) { - reset_state(); - ::setenv("SWML_ALLOW_PRIVATE_URLS", "false", 1); - stub_resolver("10.0.0.5"); - ASSERT_FALSE(validate_url("http://internal")); - reset_state(); - return true; + reset_state(); + ::setenv("SWML_ALLOW_PRIVATE_URLS", "false", 1); + stub_resolver("10.0.0.5"); + ASSERT_FALSE(validate_url("http://internal")); + reset_state(); + return true; } TEST(url_validator_blocked_networks_has_all_nine) { - ASSERT_EQ(BLOCKED_NETWORKS.size(), static_cast(9)); - return true; + ASSERT_EQ(BLOCKED_NETWORKS.size(), static_cast(9)); + return true; } diff --git a/tests/test_verb.cpp b/tests/test_verb.cpp index cc1f62b..475cddf 100644 --- a/tests/test_verb.cpp +++ b/tests/test_verb.cpp @@ -10,36 +10,36 @@ using json = nlohmann::json; // ======================================================================== TEST(verb_add_pre_answer_verb) { - AgentBase agent; - agent.add_pre_answer_verb("play", json::object({{"url", "ring.mp3"}})); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main[0].contains("play")); - ASSERT_EQ(main[0]["play"]["url"].get(), "ring.mp3"); - return true; + AgentBase agent; + agent.add_pre_answer_verb("play", json::object({{"url", "https://cdn.example.com/ring.mp3"}})); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main[0].contains("play")); + ASSERT_EQ(main[0]["play"]["url"].get(), "https://cdn.example.com/ring.mp3"); + return true; } TEST(verb_multiple_pre_answer_verbs) { - AgentBase agent; - agent.add_pre_answer_verb("play", json::object({{"url", "hold.mp3"}})); - agent.add_pre_answer_verb("sleep", json(500)); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main[0].contains("play")); - ASSERT_TRUE(main[1].contains("sleep")); - // Answer should come after - ASSERT_TRUE(main[2].contains("answer")); - return true; + AgentBase agent; + agent.add_pre_answer_verb("play", json::object({{"url", "https://cdn.example.com/hold.mp3"}})); + agent.add_pre_answer_verb("sleep", json(500)); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main[0].contains("play")); + ASSERT_TRUE(main[1].contains("sleep")); + // Answer should come after + ASSERT_TRUE(main[2].contains("answer")); + return true; } TEST(verb_clear_pre_answer_verbs) { - AgentBase agent; - agent.add_pre_answer_verb("play", json::object()); - agent.clear_pre_answer_verbs(); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main[0].contains("answer")); - return true; + AgentBase agent; + agent.add_pre_answer_verb("play", json::object()); + agent.clear_pre_answer_verbs(); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main[0].contains("answer")); + return true; } // ======================================================================== @@ -47,40 +47,41 @@ TEST(verb_clear_pre_answer_verbs) { // ======================================================================== TEST(verb_add_post_answer_verb) { - AgentBase agent; - agent.add_post_answer_verb("play", json::object({{"url", "welcome.mp3"}})); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - // Sequence: answer, play, ai - ASSERT_TRUE(main[0].contains("answer")); - ASSERT_TRUE(main[1].contains("play")); - ASSERT_TRUE(main[2].contains("ai")); - return true; + AgentBase agent; + agent.add_post_answer_verb("play", + json::object({{"url", "https://cdn.example.com/welcome.mp3"}})); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + // Sequence: answer, play, ai + ASSERT_TRUE(main[0].contains("answer")); + ASSERT_TRUE(main[1].contains("play")); + ASSERT_TRUE(main[2].contains("ai")); + return true; } TEST(verb_multiple_post_answer_verbs) { - AgentBase agent; - agent.add_post_answer_verb("play", json::object({{"url", "beep.mp3"}})); - agent.add_post_answer_verb("sleep", json(200)); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - // answer(0), play(1), sleep(2), ai(3) - ASSERT_TRUE(main[1].contains("play")); - ASSERT_TRUE(main[2].contains("sleep")); - ASSERT_TRUE(main[3].contains("ai")); - return true; + AgentBase agent; + agent.add_post_answer_verb("play", json::object({{"url", "https://cdn.example.com/beep.mp3"}})); + agent.add_post_answer_verb("sleep", json(200)); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + // answer(0), play(1), sleep(2), ai(3) + ASSERT_TRUE(main[1].contains("play")); + ASSERT_TRUE(main[2].contains("sleep")); + ASSERT_TRUE(main[3].contains("ai")); + return true; } TEST(verb_clear_post_answer_verbs) { - AgentBase agent; - agent.add_post_answer_verb("play", json::object()); - agent.clear_post_answer_verbs(); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - // answer(0), ai(1) — no play in between - ASSERT_TRUE(main[0].contains("answer")); - ASSERT_TRUE(main[1].contains("ai")); - return true; + AgentBase agent; + agent.add_post_answer_verb("play", json::object()); + agent.clear_post_answer_verbs(); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + // answer(0), ai(1) — no play in between + ASSERT_TRUE(main[0].contains("answer")); + ASSERT_TRUE(main[1].contains("ai")); + return true; } // ======================================================================== @@ -88,34 +89,34 @@ TEST(verb_clear_post_answer_verbs) { // ======================================================================== TEST(verb_add_post_ai_verb) { - AgentBase agent; - agent.add_post_ai_verb("hangup", json::object()); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main.back().contains("hangup")); - return true; + AgentBase agent; + agent.add_post_ai_verb("hangup", json::object()); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main.back().contains("hangup")); + return true; } TEST(verb_multiple_post_ai_verbs) { - AgentBase agent; - agent.add_post_ai_verb("play", json::object({{"url", "goodbye.mp3"}})); - agent.add_post_ai_verb("hangup", json::object()); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - // ai should be followed by play then hangup - ASSERT_TRUE(main[main.size() - 2].contains("play")); - ASSERT_TRUE(main[main.size() - 1].contains("hangup")); - return true; + AgentBase agent; + agent.add_post_ai_verb("play", json::object({{"url", "https://cdn.example.com/goodbye.mp3"}})); + agent.add_post_ai_verb("hangup", json::object()); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + // ai should be followed by play then hangup + ASSERT_TRUE(main[main.size() - 2].contains("play")); + ASSERT_TRUE(main[main.size() - 1].contains("hangup")); + return true; } TEST(verb_clear_post_ai_verbs) { - AgentBase agent; - agent.add_post_ai_verb("hangup", json::object()); - agent.clear_post_ai_verbs(); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main.back().contains("ai")); - return true; + AgentBase agent; + agent.add_post_ai_verb("hangup", json::object()); + agent.clear_post_ai_verbs(); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main.back().contains("ai")); + return true; } // ======================================================================== @@ -123,22 +124,22 @@ TEST(verb_clear_post_ai_verbs) { // ======================================================================== TEST(verb_default_answer_verb) { - AgentBase agent; - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main[0].contains("answer")); - ASSERT_EQ(main[0]["answer"]["max_duration"].get(), 3600); - return true; + AgentBase agent; + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main[0].contains("answer")); + ASSERT_EQ(main[0]["answer"]["max_duration"].get(), 3600); + return true; } TEST(verb_custom_answer_verb) { - AgentBase agent; - agent.add_answer_verb("answer", json::object({{"max_duration", 7200}})); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - ASSERT_TRUE(main[0].contains("answer")); - ASSERT_EQ(main[0]["answer"]["max_duration"].get(), 7200); - return true; + AgentBase agent; + agent.add_answer_verb("answer", json::object({{"max_duration", 7200}})); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + ASSERT_TRUE(main[0].contains("answer")); + ASSERT_EQ(main[0]["answer"]["max_duration"].get(), 7200); + return true; } // ======================================================================== @@ -146,32 +147,33 @@ TEST(verb_custom_answer_verb) { // ======================================================================== TEST(verb_full_5_phase_pipeline) { - AgentBase agent; - agent.add_pre_answer_verb("play", json::object({{"url", "ring.mp3"}})); - agent.add_answer_verb("answer", json::object({{"max_duration", 1800}})); - agent.add_post_answer_verb("play", json::object({{"url", "welcome.mp3"}})); - agent.add_post_ai_verb("hangup", json::object()); - agent.set_prompt_text("Hello"); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - - // Phase 1: pre-answer - ASSERT_TRUE(main[0].contains("play")); - ASSERT_EQ(main[0]["play"]["url"].get(), "ring.mp3"); - // Phase 2: answer - ASSERT_TRUE(main[1].contains("answer")); - ASSERT_EQ(main[1]["answer"]["max_duration"].get(), 1800); - // Phase 3: post-answer - ASSERT_TRUE(main[2].contains("play")); - ASSERT_EQ(main[2]["play"]["url"].get(), "welcome.mp3"); - // Phase 4: ai - ASSERT_TRUE(main[3].contains("ai")); - // Phase 5: post-ai - ASSERT_TRUE(main[4].contains("hangup")); - - ASSERT_EQ(main.size(), 5u); - return true; + AgentBase agent; + agent.add_pre_answer_verb("play", json::object({{"url", "https://cdn.example.com/ring.mp3"}})); + agent.add_answer_verb("answer", json::object({{"max_duration", 1800}})); + agent.add_post_answer_verb("play", + json::object({{"url", "https://cdn.example.com/welcome.mp3"}})); + agent.add_post_ai_verb("hangup", json::object()); + agent.set_prompt_text("Hello"); + + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + + // Phase 1: pre-answer + ASSERT_TRUE(main[0].contains("play")); + ASSERT_EQ(main[0]["play"]["url"].get(), "https://cdn.example.com/ring.mp3"); + // Phase 2: answer + ASSERT_TRUE(main[1].contains("answer")); + ASSERT_EQ(main[1]["answer"]["max_duration"].get(), 1800); + // Phase 3: post-answer + ASSERT_TRUE(main[2].contains("play")); + ASSERT_EQ(main[2]["play"]["url"].get(), "https://cdn.example.com/welcome.mp3"); + // Phase 4: ai + ASSERT_TRUE(main[3].contains("ai")); + // Phase 5: post-ai + ASSERT_TRUE(main[4].contains("hangup")); + + ASSERT_EQ(main.size(), 5u); + return true; } // ======================================================================== @@ -179,10 +181,13 @@ TEST(verb_full_5_phase_pipeline) { // ======================================================================== TEST(verb_method_chaining) { - AgentBase agent; - auto& ref = agent.add_pre_answer_verb("play", json::object()) - .add_post_answer_verb("sleep", json(100)) - .add_post_ai_verb("hangup", json::object()); - ASSERT_EQ(&ref, &agent); - return true; + AgentBase agent; + // `play {}` is schema-INVALID (PlayWithURL/PlayWithURLS require url/urls), + // and the render now goes through the validator, so use a real url. + auto& ref = + agent.add_pre_answer_verb("play", json::object({{"url", "https://cdn.example.com/r.mp3"}})) + .add_post_answer_verb("sleep", json(100)) + .add_post_ai_verb("hangup", json::object()); + ASSERT_EQ(&ref, &agent); + return true; } diff --git a/tests/test_web.cpp b/tests/test_web.cpp index 2e5685d..7d7066e 100644 --- a/tests/test_web.cpp +++ b/tests/test_web.cpp @@ -11,42 +11,49 @@ using json = nlohmann::json; // ======================================================================== TEST(web_manual_proxy_url) { - AgentBase agent; - agent.manual_set_proxy_url("https://proxy.example.com"); - agent.set_auth("u", "p"); - agent.define_tool("test_tool", "Test", json::object(), - [](const json&, const json&) { return signalwire::swaig::FunctionResult("ok"); }); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); - ASSERT_TRUE(url.find("proxy.example.com") != std::string::npos); - return true; - } + AgentBase agent; + agent.manual_set_proxy_url("https://proxy.example.com"); + agent.set_auth("u", "p"); + agent.define_tool("test_tool", "Test", json::object(), [](const json&, const json&) { + return signalwire::swaig::FunctionResult("ok"); + }); + + // Render WITH a call_id: a per-tool web_hook_url is only emitted when the + // entry carries a token (or SWAIG query params) — reference + // agent_base.py:1085-1099. Without one there is no URL to inspect at all. + const std::map q = {{"call_id", "call-abc"}}; + json swml = agent.render_swml_for_request(q, json::object(), {}); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); + ASSERT_TRUE(url.find("proxy.example.com") != std::string::npos); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } TEST(web_webhook_url_override) { - AgentBase agent; - agent.set_webhook_url("https://custom.webhook.com/swaig"); - agent.set_auth("u", "p"); - agent.define_tool("test_tool", "Test", json::object(), nullptr); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); - ASSERT_EQ(url, "https://custom.webhook.com/swaig"); - return true; - } + AgentBase agent; + agent.set_webhook_url("https://custom.webhook.com/swaig"); + agent.set_auth("u", "p"); + agent.define_tool("test_tool", "Test", json::object(), nullptr); + + // Render WITH a call_id — see web_manual_proxy_url. + const std::map q = {{"call_id", "call-abc"}}; + json swml = agent.render_swml_for_request(q, json::object(), {}); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); + ASSERT_TRUE(url.rfind("https://custom.webhook.com/swaig", 0) == 0); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -54,44 +61,47 @@ TEST(web_webhook_url_override) { // ======================================================================== TEST(web_swaig_query_params) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.add_swaig_query_param("tenant", "acme"); - agent.add_swaig_query_param("mode", "test"); - agent.define_tool("test_tool", "Test", json::object(), nullptr); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); - ASSERT_TRUE(url.find("tenant=acme") != std::string::npos); - ASSERT_TRUE(url.find("mode=test") != std::string::npos); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.add_swaig_query_param("tenant", "acme"); + agent.add_swaig_query_param("mode", "test"); + agent.define_tool("test_tool", "Test", json::object(), nullptr); + + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); + ASSERT_TRUE(url.find("tenant=acme") != std::string::npos); + ASSERT_TRUE(url.find("mode=test") != std::string::npos); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } TEST(web_clear_swaig_query_params) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.add_swaig_query_param("key", "val"); - agent.clear_swaig_query_params(); - agent.define_tool("test_tool", "Test", json::object(), nullptr); - - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); - ASSERT_TRUE(url.find("key=val") == std::string::npos); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.add_swaig_query_param("key", "val"); + agent.clear_swaig_query_params(); + agent.define_tool("test_tool", "Test", json::object(), nullptr); + + // Render WITH a call_id — with the params cleared, the token is now the only + // thing that earns this entry its own web_hook_url (see web_manual_proxy_url). + const std::map q = {{"call_id", "call-abc"}}; + json swml = agent.render_swml_for_request(q, json::object(), {}); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); + ASSERT_TRUE(url.find("key=val") == std::string::npos); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -99,53 +109,50 @@ TEST(web_clear_swaig_query_params) { // ======================================================================== TEST(web_dynamic_config_modifies_copy) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.set_prompt_text("Original prompt"); - - agent.set_dynamic_config_callback( - [](const std::map& qp, - const json&, - const std::map&, - AgentBase& copy) { - auto it = qp.find("tenant"); - if (it != qp.end()) { - copy.set_prompt_text("Tenant: " + it->second); - } - }); - - std::map qp = {{"tenant", "acme"}}; - json swml = agent.render_swml_for_request(qp, json::object(), {}); - - // Original agent should not change - ASSERT_EQ(agent.get_prompt(), "Original prompt"); - - // Rendered SWML should have the modified prompt - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("prompt")) { - auto text = verb["ai"]["prompt"]["text"].get(); - ASSERT_EQ(text, "Tenant: acme"); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.set_prompt_text("Original prompt"); + + agent.set_dynamic_config_callback([](const std::map& qp, const json&, + const std::map&, AgentBase& copy) { + auto it = qp.find("tenant"); + if (it != qp.end()) { + copy.set_prompt_text("Tenant: " + it->second); } - ASSERT_TRUE(false); - return true; + }); + + std::map qp = {{"tenant", "acme"}}; + json swml = agent.render_swml_for_request(qp, json::object(), {}); + + // Original agent should not change + ASSERT_EQ(agent.get_prompt(), "Original prompt"); + + // Rendered SWML should have the modified prompt + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("prompt")) { + auto text = verb["ai"]["prompt"]["text"].get(); + ASSERT_EQ(text, "Tenant: acme"); + return true; + } + } + ASSERT_TRUE(false); + return true; } TEST(web_dynamic_config_without_callback) { - AgentBase agent; - agent.set_prompt_text("Static"); - json swml = agent.render_swml_for_request({}, json::object(), {}); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("prompt")) { - ASSERT_EQ(verb["ai"]["prompt"]["text"].get(), "Static"); - return true; - } + AgentBase agent; + agent.set_prompt_text("Static"); + json swml = agent.render_swml_for_request({}, json::object(), {}); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("prompt")) { + ASSERT_EQ(verb["ai"]["prompt"]["text"].get(), "Static"); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -153,27 +160,27 @@ TEST(web_dynamic_config_without_callback) { // ======================================================================== TEST(web_proxy_from_forwarded_headers) { - AgentBase agent; - agent.set_auth("u", "p"); - agent.define_tool("test_tool", "Test", json::object(), nullptr); - - std::map headers = { - {"x-forwarded-proto", "https"}, - {"x-forwarded-host", "myapp.example.com"} - }; - json swml = agent.render_swml_for_request({}, json::object(), headers); - - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { - auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); - ASSERT_TRUE(url.find("myapp.example.com") != std::string::npos); - ASSERT_TRUE(url.find("https://") != std::string::npos); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + agent.define_tool("test_tool", "Test", json::object(), nullptr); + + std::map headers = {{"x-forwarded-proto", "https"}, + {"x-forwarded-host", "myapp.example.com"}}; + // Render WITH a call_id — see web_manual_proxy_url. + const std::map q = {{"call_id", "call-abc"}}; + json swml = agent.render_swml_for_request(q, json::object(), headers); + + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("SWAIG")) { + auto url = verb["ai"]["SWAIG"]["functions"][0]["web_hook_url"].get(); + ASSERT_TRUE(url.find("myapp.example.com") != std::string::npos); + ASSERT_TRUE(url.find("https://") != std::string::npos); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -181,35 +188,34 @@ TEST(web_proxy_from_forwarded_headers) { // ======================================================================== TEST(web_post_prompt_url_auto_generated) { - AgentBase agent; - agent.set_auth("u", "p"); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("post_prompt_url")) { - auto url = verb["ai"]["post_prompt_url"].get(); - ASSERT_TRUE(url.find("/post_prompt") != std::string::npos); - return true; - } + AgentBase agent; + agent.set_auth("u", "p"); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("post_prompt_url")) { + auto url = verb["ai"]["post_prompt_url"].get(); + ASSERT_TRUE(url.find("/post_prompt") != std::string::npos); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } TEST(web_post_prompt_url_direct_override) { - AgentBase agent; - agent.set_post_prompt_url_direct("https://custom.com/post_prompt"); - json swml = agent.render_swml(); - auto& main = swml["sections"]["main"]; - for (const auto& verb : main) { - if (verb.contains("ai") && verb["ai"].contains("post_prompt_url")) { - ASSERT_EQ(verb["ai"]["post_prompt_url"].get(), - "https://custom.com/post_prompt"); - return true; - } + AgentBase agent; + agent.set_post_prompt_url_direct("https://custom.com/post_prompt"); + json swml = agent.render_swml(); + auto& main = swml["sections"]["main"]; + for (const auto& verb : main) { + if (verb.contains("ai") && verb["ai"].contains("post_prompt_url")) { + ASSERT_EQ(verb["ai"]["post_prompt_url"].get(), "https://custom.com/post_prompt"); + return true; } - ASSERT_TRUE(false); - return true; + } + ASSERT_TRUE(false); + return true; } // ======================================================================== @@ -217,10 +223,10 @@ TEST(web_post_prompt_url_direct_override) { // ======================================================================== TEST(web_enable_debug_routes) { - AgentBase agent; - agent.enable_debug_routes(true); - // Just verify it doesn't crash; actual route testing requires server - return true; + AgentBase agent; + agent.enable_debug_routes(true); + // Just verify it doesn't crash; actual route testing requires server + return true; } // ======================================================================== @@ -228,26 +234,26 @@ TEST(web_enable_debug_routes) { // ======================================================================== TEST(web_sip_routing_enable) { - AgentBase agent; - agent.enable_sip_routing(true); - // No crash - return true; + AgentBase agent; + agent.enable_sip_routing(true); + // No crash + return true; } TEST(web_sip_register_valid_username) { - AgentBase agent; - agent.enable_sip_routing(true); - agent.register_sip_username("alice"); - agent.register_sip_username("bob_123"); - // No crash; valid usernames accepted - return true; + AgentBase agent; + agent.enable_sip_routing(true); + agent.register_sip_username("alice"); + agent.register_sip_username("bob_123"); + // No crash; valid usernames accepted + return true; } TEST(web_auto_map_sip_usernames) { - AgentBase agent; - agent.auto_map_sip_usernames(true); - // No crash - return true; + AgentBase agent; + agent.auto_map_sip_usernames(true); + // No crash + return true; } // ======================================================================== @@ -261,55 +267,99 @@ TEST(web_auto_map_sip_usernames) { namespace { class CustomSwmlService : public signalwire::swml::Service { -public: - json last_request_data; - std::string last_callback_path; - std::optional custom_return; - - std::optional on_swml_request( - const std::optional& request_data, - const std::optional& callback_path) override { - last_request_data = request_data.value_or(json{}); - last_callback_path = callback_path.value_or(std::string{}); - return custom_return; - } + public: + json last_request_data; + std::string last_callback_path; + std::optional last_request; + bool saw_request_arg = false; + std::optional custom_return; + + std::optional on_swml_request(const std::optional& request_data, + const std::optional& callback_path, + const std::optional& request) override { + last_request_data = request_data.value_or(json{}); + last_callback_path = callback_path.value_or(std::string{}); + last_request = request; + saw_request_arg = request.has_value(); + return custom_return; + } }; -} +} // namespace TEST(web_on_request_delegates_to_on_swml_request) { - CustomSwmlService svc; - svc.custom_return = json{{"custom", true}}; + CustomSwmlService svc; + svc.custom_return = json{{"custom", true}}; + + json rd{{"data", "val"}}; + auto result = svc.on_request(rd, std::string{"/cb"}); + + ASSERT_EQ(svc.last_request_data, rd); + ASSERT_EQ(svc.last_callback_path, std::string{"/cb"}); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ((*result)["custom"].get(), true); + // on_request carries no request object; the reference passes None from this + // path (web_mixin.py:1342), so the override must observe an empty optional. + ASSERT_FALSE(svc.saw_request_arg); + return true; +} - json rd{{"data", "val"}}; - auto result = svc.on_request(rd, std::string{"/cb"}); +// The third parameter is the whole point of widening this hook: before it +// existed, a C++ subclass overriding on_swml_request could not reach the +// inbound request AT ALL, so query params and headers were invisible to the +// dispatch hook. This asserts the argument actually arrives at the override. +TEST(web_on_swml_request_receives_the_request_object) { + CustomSwmlService svc; + json req{{"query_params", {{"tenant", "acme"}}}, {"headers", {{"x-trace", "abc123"}}}}; + + auto result = svc.on_swml_request(json{{"data", "val"}}, std::string{"/cb"}, req); + (void)result; + + ASSERT_TRUE(svc.saw_request_arg); + ASSERT_TRUE(svc.last_request.has_value()); + ASSERT_EQ((*svc.last_request)["query_params"]["tenant"].get(), std::string{"acme"}); + ASSERT_EQ((*svc.last_request)["headers"]["x-trace"].get(), std::string{"abc123"}); + return true; +} - ASSERT_EQ(svc.last_request_data, rd); - ASSERT_EQ(svc.last_callback_path, std::string{"/cb"}); - ASSERT_TRUE(result.has_value()); - ASSERT_EQ((*result)["custom"].get(), true); - return true; +// The base is virtual and the prefab now genuinely OVERRIDES it (it used to +// take `const json&` / return `json`, matching neither arity nor return type, +// so it HID the base and virtual dispatch never reached it). +TEST(web_on_swml_request_dispatches_virtually_through_a_base_reference) { + CustomSwmlService svc; + svc.custom_return = json{{"via", "base-ref"}}; + signalwire::swml::Service& base = svc; + + json req{{"query_params", {{"k", "v"}}}, {"headers", json::object()}}; + auto result = base.on_swml_request(std::nullopt, std::nullopt, req); + + ASSERT_TRUE(result.has_value()); + ASSERT_EQ((*result)["via"].get(), std::string{"base-ref"}); + ASSERT_TRUE(svc.saw_request_arg); + ASSERT_TRUE(svc.last_request.has_value()); + ASSERT_EQ((*svc.last_request)["query_params"]["k"].get(), std::string{"v"}); + return true; } TEST(web_on_swml_request_default_returns_nullopt) { - signalwire::swml::Service svc; - auto result = svc.on_swml_request(std::nullopt, std::nullopt); - ASSERT_FALSE(result.has_value()); - return true; + signalwire::swml::Service svc; + auto result = svc.on_swml_request(std::nullopt, std::nullopt); + ASSERT_FALSE(result.has_value()); + return true; } TEST(web_on_request_default_returns_nullopt) { - signalwire::swml::Service svc; - auto result = svc.on_request(std::nullopt, std::nullopt); - ASSERT_FALSE(result.has_value()); - return true; + signalwire::swml::Service svc; + auto result = svc.on_request(std::nullopt, std::nullopt); + ASSERT_FALSE(result.has_value()); + return true; } TEST(web_on_request_passes_nulls_to_hook) { - CustomSwmlService svc; - svc.custom_return = std::nullopt; - auto result = svc.on_request(std::nullopt, std::nullopt); - ASSERT_FALSE(result.has_value()); - ASSERT_EQ(svc.last_request_data, json{}); - ASSERT_EQ(svc.last_callback_path, std::string{}); - return true; + CustomSwmlService svc; + svc.custom_return = std::nullopt; + auto result = svc.on_request(std::nullopt, std::nullopt); + ASSERT_FALSE(result.has_value()); + ASSERT_EQ(svc.last_request_data, json{}); + ASSERT_EQ(svc.last_callback_path, std::string{}); + return true; } diff --git a/tests/test_webhook_middleware.cpp b/tests/test_webhook_middleware.cpp index 1c11737..0153925 100644 --- a/tests/test_webhook_middleware.cpp +++ b/tests/test_webhook_middleware.cpp @@ -5,12 +5,8 @@ // post valid / invalid / missing-signature requests, and assert the // response code and body forwarding behavior. -#include "signalwire/security/webhook_middleware.hpp" -#include "signalwire/security/webhook_validator.hpp" -#include "signalwire/agent/agent_base.hpp" -#include "httplib.h" - #include + #include #include #include @@ -19,99 +15,106 @@ #include #include +#include "httplib.h" +#include "signalwire/agent/agent_base.hpp" +#include "signalwire/security/webhook_middleware.hpp" +#include "signalwire/security/webhook_validator.hpp" + using namespace signalwire::security; namespace { std::string mw_b64(const std::string& data) { - static const char table[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - int val = 0, valb = -6; - for (unsigned char c : data) { - val = (val << 8) + c; - valb += 8; - while (valb >= 0) { - out.push_back(table[(val >> valb) & 0x3F]); - valb -= 6; - } + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + int val = 0, valb = -6; + for (unsigned char c : data) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(table[(val >> valb) & 0x3F]); + valb -= 6; } - if (valb > -6) out.push_back(table[((val << 8) >> (valb + 8)) & 0x3F]); - while (out.size() % 4) out.push_back('='); - return out; + } + if (valb > -6) { + out.push_back(table[((val << 8) >> (valb + 8)) & 0x3F]); + } + while (out.size() % 4) { + out.push_back('='); + } + return out; } std::string mw_hmac_sha1_hex(const std::string& key, const std::string& msg) { - unsigned char out[EVP_MAX_MD_SIZE]; - unsigned int out_len = 0; - HMAC(EVP_sha1(), - key.data(), static_cast(key.size()), - reinterpret_cast(msg.data()), msg.size(), - out, &out_len); - std::ostringstream ss; - ss << std::hex << std::setfill('0'); - for (unsigned int i = 0; i < out_len; ++i) { - ss << std::setw(2) << static_cast(out[i]); - } - return ss.str(); + unsigned char out[EVP_MAX_MD_SIZE]; + unsigned int out_len = 0; + HMAC(EVP_sha1(), key.data(), static_cast(key.size()), + reinterpret_cast(msg.data()), msg.size(), out, &out_len); + std::ostringstream ss; + ss << std::hex << std::setfill('0'); + for (unsigned int i = 0; i < out_len; ++i) { + ss << std::setw(2) << static_cast(out[i]); + } + return ss.str(); } /// RAII test server: binds to 127.0.0.1 on an ephemeral port, runs in a /// background thread, stops + joins on destruction. Mirrors the pattern /// used by tests/test_skill_websearch.cpp et al. struct TestServer { - httplib::Server srv; - std::thread th; - int port = 0; - - TestServer() = default; - - void start() { - th = std::thread([this] { - port = srv.bind_to_any_port("127.0.0.1"); - srv.listen_after_bind(); - }); - auto deadline = std::chrono::steady_clock::now() + - std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } + httplib::Server srv; + std::thread th; + int port = 0; + + TestServer() = default; + + void start() { + th = std::thread([this] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } + } - ~TestServer() { - srv.stop(); - if (th.joinable()) th.join(); + ~TestServer() { + srv.stop(); + if (th.joinable()) { + th.join(); } + } }; -} // namespace +} // namespace // =========================================================================== // Helper-shape sanity: the middleware throws on bad construction. // =========================================================================== TEST(webhook_middleware_empty_key_throws) { - auto h = [](const httplib::Request&, httplib::Response&) {}; - bool threw = false; - try { - (void)WrapWithSignatureValidation("", h); - } catch (const std::invalid_argument&) { - threw = true; - } - ASSERT_TRUE(threw); - return true; + auto h = [](const httplib::Request&, httplib::Response&) {}; + bool threw = false; + try { + (void)WrapWithSignatureValidation("", h); + } catch (const std::invalid_argument&) { + threw = true; + } + ASSERT_TRUE(threw); + return true; } TEST(webhook_middleware_null_handler_throws) { - HttpHandler null_h; - bool threw = false; - try { - (void)WrapWithSignatureValidation("key", null_h); - } catch (const std::invalid_argument&) { - threw = true; - } - ASSERT_TRUE(threw); - return true; + HttpHandler null_h; + bool threw = false; + try { + (void)WrapWithSignatureValidation("key", null_h); + } catch (const std::invalid_argument&) { + threw = true; + } + ASSERT_TRUE(threw); + return true; } // =========================================================================== @@ -119,127 +122,122 @@ TEST(webhook_middleware_null_handler_throws) { // =========================================================================== TEST(webhook_middleware_valid_signature_calls_handler_and_forwards_body) { - std::string key = "PSKtest1234567890abcdef"; - std::string body = R"({"event":"call.state","ok":true})"; - - TestServer ts; - std::atomic handler_called{false}; - std::string captured_body; - auto downstream = [&handler_called, &captured_body]( - const httplib::Request& req, httplib::Response& res) { - handler_called = true; - captured_body = req.body; - res.status = 200; - res.set_content("OK", "text/plain"); - }; - - WebhookValidatorOptions opts; - // Force the URL the middleware reconstructs so we can sign for it. - opts.proxy_url_base = "http://localhost.test"; - auto wrapped = WrapWithSignatureValidation(key, downstream, opts); - ts.srv.Post("/webhook", wrapped); - ts.start(); - ASSERT_TRUE(ts.port > 0); - - std::string url = "http://localhost.test/webhook"; - std::string sig = mw_hmac_sha1_hex(key, url + body); - - httplib::Client cli("127.0.0.1", ts.port); - httplib::Headers hdrs = {{"X-SignalWire-Signature", sig}}; - auto resp = cli.Post("/webhook", hdrs, body, "application/json"); - - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 200); - ASSERT_TRUE(handler_called.load()); - ASSERT_EQ(captured_body, body); // raw bytes forwarded unmodified - return true; + std::string key = "PSKtest1234567890abcdef"; + std::string body = R"({"event":"call.state","ok":true})"; + + TestServer ts; + std::atomic handler_called{false}; + std::string captured_body; + auto downstream = [&handler_called, &captured_body](const httplib::Request& req, + httplib::Response& res) { + handler_called = true; + captured_body = req.body; + res.status = 200; + res.set_content("OK", "text/plain"); + }; + + WebhookValidatorOptions opts; + // Force the URL the middleware reconstructs so we can sign for it. + opts.proxy_url_base = "http://localhost.test"; + auto wrapped = WrapWithSignatureValidation(key, downstream, opts); + ts.srv.Post("/webhook", wrapped); + ts.start(); + ASSERT_TRUE(ts.port > 0); + + std::string url = "http://localhost.test/webhook"; + std::string sig = mw_hmac_sha1_hex(key, url + body); + + httplib::Client cli("127.0.0.1", ts.port); + httplib::Headers hdrs = {{"X-SignalWire-Signature", sig}}; + auto resp = cli.Post("/webhook", hdrs, body, "application/json"); + + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 200); + ASSERT_TRUE(handler_called.load()); + ASSERT_EQ(captured_body, body); // raw bytes forwarded unmodified + return true; } TEST(webhook_middleware_invalid_signature_returns_403) { - std::string key = "PSKtest1234567890abcdef"; - - TestServer ts; - std::atomic handler_called{false}; - auto downstream = [&handler_called](const httplib::Request&, - httplib::Response& res) { - handler_called = true; - res.status = 200; - res.set_content("OK", "text/plain"); - }; - - WebhookValidatorOptions opts; - opts.proxy_url_base = "http://localhost.test"; - ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); - ts.start(); - ASSERT_TRUE(ts.port > 0); - - httplib::Client cli("127.0.0.1", ts.port); - httplib::Headers hdrs = {{"X-SignalWire-Signature", "bogus-not-the-right-sig"}}; - auto resp = cli.Post("/webhook", hdrs, R"({"event":"call.state"})", - "application/json"); - - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 403); - ASSERT_FALSE(handler_called.load()); // downstream MUST NOT run on 403 - return true; + std::string key = "PSKtest1234567890abcdef"; + + TestServer ts; + std::atomic handler_called{false}; + auto downstream = [&handler_called](const httplib::Request&, httplib::Response& res) { + handler_called = true; + res.status = 200; + res.set_content("OK", "text/plain"); + }; + + WebhookValidatorOptions opts; + opts.proxy_url_base = "http://localhost.test"; + ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); + ts.start(); + ASSERT_TRUE(ts.port > 0); + + httplib::Client cli("127.0.0.1", ts.port); + httplib::Headers hdrs = {{"X-SignalWire-Signature", "bogus-not-the-right-sig"}}; + auto resp = cli.Post("/webhook", hdrs, R"({"event":"call.state"})", "application/json"); + + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 403); + ASSERT_FALSE(handler_called.load()); // downstream MUST NOT run on 403 + return true; } TEST(webhook_middleware_missing_signature_returns_403) { - std::string key = "PSKtest1234567890abcdef"; - - TestServer ts; - std::atomic handler_called{false}; - auto downstream = [&handler_called](const httplib::Request&, - httplib::Response& res) { - handler_called = true; - res.status = 200; - }; - WebhookValidatorOptions opts; - opts.proxy_url_base = "http://localhost.test"; - ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); - ts.start(); - ASSERT_TRUE(ts.port > 0); - - httplib::Client cli("127.0.0.1", ts.port); - auto resp = cli.Post("/webhook", R"({"event":"call.state"})", - "application/json"); - - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 403); - ASSERT_FALSE(handler_called.load()); - return true; + std::string key = "PSKtest1234567890abcdef"; + + TestServer ts; + std::atomic handler_called{false}; + auto downstream = [&handler_called](const httplib::Request&, httplib::Response& res) { + handler_called = true; + res.status = 200; + }; + WebhookValidatorOptions opts; + opts.proxy_url_base = "http://localhost.test"; + ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); + ts.start(); + ASSERT_TRUE(ts.port > 0); + + httplib::Client cli("127.0.0.1", ts.port); + auto resp = cli.Post("/webhook", R"({"event":"call.state"})", "application/json"); + + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 403); + ASSERT_FALSE(handler_called.load()); + return true; } TEST(webhook_middleware_accepts_x_twilio_signature_legacy_header) { - // cXML compat: legacy callers send X-Twilio-Signature; the spec says - // ports SHOULD honor it as an alias of X-SignalWire-Signature. - std::string key = "PSKtest1234567890abcdef"; - std::string body = R"({"legacy":"yes"})"; - - TestServer ts; - std::atomic handler_called{false}; - auto downstream = [&handler_called](const httplib::Request&, - httplib::Response& res) { - handler_called = true; - res.status = 200; - }; - WebhookValidatorOptions opts; - opts.proxy_url_base = "http://localhost.test"; - ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); - ts.start(); - ASSERT_TRUE(ts.port > 0); - - std::string url = "http://localhost.test/webhook"; - std::string sig = mw_hmac_sha1_hex(key, url + body); - - httplib::Client cli("127.0.0.1", ts.port); - httplib::Headers hdrs = {{"X-Twilio-Signature", sig}}; - auto resp = cli.Post("/webhook", hdrs, body, "application/json"); - - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 200); - ASSERT_TRUE(handler_called.load()); - return true; + // cXML compat: legacy callers send X-Twilio-Signature; the spec says + // ports SHOULD honor it as an alias of X-SignalWire-Signature. + std::string key = "PSKtest1234567890abcdef"; + std::string body = R"({"legacy":"yes"})"; + + TestServer ts; + std::atomic handler_called{false}; + auto downstream = [&handler_called](const httplib::Request&, httplib::Response& res) { + handler_called = true; + res.status = 200; + }; + WebhookValidatorOptions opts; + opts.proxy_url_base = "http://localhost.test"; + ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); + ts.start(); + ASSERT_TRUE(ts.port > 0); + + std::string url = "http://localhost.test/webhook"; + std::string sig = mw_hmac_sha1_hex(key, url + body); + + httplib::Client cli("127.0.0.1", ts.port); + httplib::Headers hdrs = {{"X-Twilio-Signature", sig}}; + auto resp = cli.Post("/webhook", hdrs, body, "application/json"); + + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 200); + ASSERT_TRUE(handler_called.load()); + return true; } // =========================================================================== @@ -250,62 +248,62 @@ TEST(webhook_middleware_accepts_x_twilio_signature_legacy_header) { // =========================================================================== TEST(webhook_agent_signing_key_unset_by_default) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - signalwire::agent::AgentBase agent("a", "/a"); - ASSERT_FALSE(agent.signing_key().has_value()); - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + signalwire::agent::AgentBase agent("a", "/a"); + ASSERT_FALSE(agent.signing_key().has_value()); + return true; } TEST(webhook_agent_set_signing_key_stores_value) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - signalwire::agent::AgentBase agent("a", "/a"); - agent.set_signing_key("PSKtest-explicit-key"); - auto got = agent.signing_key(); - ASSERT_TRUE(got.has_value()); - ASSERT_EQ(*got, std::string("PSKtest-explicit-key")); - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + signalwire::agent::AgentBase agent("a", "/a"); + agent.set_signing_key("PSKtest-explicit-key"); + auto got = agent.signing_key(); + ASSERT_TRUE(got.has_value()); + ASSERT_EQ(*got, std::string("PSKtest-explicit-key")); + return true; } TEST(webhook_agent_set_signing_key_empty_clears) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - signalwire::agent::AgentBase agent("a", "/a"); - agent.set_signing_key("PSKtest"); - ASSERT_TRUE(agent.signing_key().has_value()); - agent.set_signing_key(""); - ASSERT_FALSE(agent.signing_key().has_value()); - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + signalwire::agent::AgentBase agent("a", "/a"); + agent.set_signing_key("PSKtest"); + ASSERT_TRUE(agent.signing_key().has_value()); + agent.set_signing_key(""); + ASSERT_FALSE(agent.signing_key().has_value()); + return true; } TEST(webhook_agent_picks_up_env_var_at_construction) { - ::setenv("SIGNALWIRE_SIGNING_KEY", "PSKtest-from-env", 1); - signalwire::agent::AgentBase agent("a", "/a"); - auto got = agent.signing_key(); - ASSERT_TRUE(got.has_value()); - ASSERT_EQ(*got, std::string("PSKtest-from-env")); - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - return true; + ::setenv("SIGNALWIRE_SIGNING_KEY", "PSKtest-from-env", 1); + signalwire::agent::AgentBase agent("a", "/a"); + auto got = agent.signing_key(); + ASSERT_TRUE(got.has_value()); + ASSERT_EQ(*got, std::string("PSKtest-from-env")); + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + return true; } TEST(webhook_agent_explicit_overrides_env) { - ::setenv("SIGNALWIRE_SIGNING_KEY", "PSKtest-from-env", 1); - signalwire::agent::AgentBase agent("a", "/a"); - agent.set_signing_key("PSKtest-explicit"); - auto got = agent.signing_key(); - ASSERT_TRUE(got.has_value()); - ASSERT_EQ(*got, std::string("PSKtest-explicit")); - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - return true; + ::setenv("SIGNALWIRE_SIGNING_KEY", "PSKtest-from-env", 1); + signalwire::agent::AgentBase agent("a", "/a"); + agent.set_signing_key("PSKtest-explicit"); + auto got = agent.signing_key(); + ASSERT_TRUE(got.has_value()); + ASSERT_EQ(*got, std::string("PSKtest-explicit")); + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + return true; } TEST(webhook_agent_trust_proxy_for_signature_default_false) { - // Default is false (proxy headers spoofable; opt-in only). The flag - // is plumbed through to the middleware in setup_routes — this test - // documents the default for users. - signalwire::agent::AgentBase agent("a", "/a"); - auto& chain = agent.trust_proxy_for_signature(true); - // Method returns *this for chaining. - ASSERT_TRUE(&chain == &agent); - return true; + // Default is false (proxy headers spoofable; opt-in only). The flag + // is plumbed through to the middleware in setup_routes — this test + // documents the default for users. + signalwire::agent::AgentBase agent("a", "/a"); + auto& chain = agent.trust_proxy_for_signature(true); + // Method returns *this for chaining. + ASSERT_TRUE(&chain == &agent); + return true; } // =========================================================================== @@ -318,149 +316,148 @@ TEST(webhook_agent_trust_proxy_for_signature_default_false) { namespace { struct AgentServerHarness { - httplib::Server srv; - std::thread th; - int port = 0; - std::shared_ptr agent; - - void start() { - th = std::thread([this] { - port = srv.bind_to_any_port("127.0.0.1"); - srv.listen_after_bind(); - }); - auto deadline = std::chrono::steady_clock::now() + - std::chrono::seconds(3); - while (port == 0 && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } + httplib::Server srv; + std::thread th; + int port = 0; + std::shared_ptr agent; + + void start() { + th = std::thread([this] { + port = srv.bind_to_any_port("127.0.0.1"); + srv.listen_after_bind(); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (port == 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - ~AgentServerHarness() { - srv.stop(); - if (th.joinable()) th.join(); + } + ~AgentServerHarness() { + srv.stop(); + if (th.joinable()) { + th.join(); } + } }; -} // namespace +} // namespace TEST(webhook_agent_unsigned_post_rejected_when_key_set) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - AgentServerHarness h; - h.agent = std::make_shared("a", "/a"); - h.agent->set_auth("u", "p"); - h.agent->set_signing_key("PSKtest-agent-key"); - - // Wire up the agent's routes onto our test server using setup_routes - // (the same code path serve()/AgentServer use). - struct Friend : signalwire::agent::AgentBase { - using signalwire::agent::AgentBase::setup_routes; - using signalwire::agent::AgentBase::init_auth; - }; - auto& f = static_cast(*h.agent); - f.init_auth(); - f.setup_routes(h.srv); - h.start(); - ASSERT_TRUE(h.port > 0); - - httplib::Client cli("127.0.0.1", h.port); - cli.set_basic_auth("u", "p"); - // Unsigned POST to /a/swaig should 403 (signing_key is set). - auto resp = cli.Post("/a/swaig", "{}", "application/json"); - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 403); - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + AgentServerHarness h; + h.agent = std::make_shared("a", "/a"); + h.agent->set_auth("u", "p"); + h.agent->set_signing_key("PSKtest-agent-key"); + + // Wire up the agent's routes onto our test server using setup_routes + // (the same code path serve()/AgentServer use). + struct Friend : signalwire::agent::AgentBase { + using signalwire::agent::AgentBase::init_auth; + using signalwire::agent::AgentBase::setup_routes; + }; + auto& f = static_cast(*h.agent); + f.init_auth(); + f.setup_routes(h.srv); + h.start(); + ASSERT_TRUE(h.port > 0); + + httplib::Client cli("127.0.0.1", h.port); + cli.set_basic_auth("u", "p"); + // Unsigned POST to /a/swaig should 403 (signing_key is set). + auto resp = cli.Post("/a/swaig", "{}", "application/json"); + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 403); + return true; } TEST(webhook_agent_unsigned_post_accepted_when_key_unset) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - AgentServerHarness h; - h.agent = std::make_shared("b", "/b"); - h.agent->set_auth("u", "p"); - // No signing key set — unsigned POSTs should reach the handler. - - struct Friend : signalwire::agent::AgentBase { - using signalwire::agent::AgentBase::setup_routes; - using signalwire::agent::AgentBase::init_auth; - }; - auto& f = static_cast(*h.agent); - f.init_auth(); - f.setup_routes(h.srv); - h.start(); - ASSERT_TRUE(h.port > 0); - - httplib::Client cli("127.0.0.1", h.port); - cli.set_basic_auth("u", "p"); - // POST to /b/swaig with empty body — handler should run and respond - // 400 ("empty body") rather than 403. - auto resp = cli.Post("/b/swaig", "", "application/json"); - ASSERT_TRUE((bool)resp); - ASSERT_NE(resp->status, 403); // not blocked by signature middleware - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + AgentServerHarness h; + h.agent = std::make_shared("b", "/b"); + h.agent->set_auth("u", "p"); + // No signing key set — unsigned POSTs should reach the handler. + + struct Friend : signalwire::agent::AgentBase { + using signalwire::agent::AgentBase::init_auth; + using signalwire::agent::AgentBase::setup_routes; + }; + auto& f = static_cast(*h.agent); + f.init_auth(); + f.setup_routes(h.srv); + h.start(); + ASSERT_TRUE(h.port > 0); + + httplib::Client cli("127.0.0.1", h.port); + cli.set_basic_auth("u", "p"); + // POST to /b/swaig with empty body — handler should run and respond + // 400 ("empty body") rather than 403. + auto resp = cli.Post("/b/swaig", "", "application/json"); + ASSERT_TRUE((bool)resp); + ASSERT_NE(resp->status, 403); // not blocked by signature middleware + return true; } TEST(webhook_agent_signed_post_passes_through) { - ::unsetenv("SIGNALWIRE_SIGNING_KEY"); - AgentServerHarness h; - h.agent = std::make_shared("c", "/c"); - h.agent->set_auth("u", "p"); - std::string key = "PSKtest-agent-signed-pass"; - h.agent->set_signing_key(key); - // Tell the middleware to honor X-Forwarded-* so we can sign for a - // specific public URL. - h.agent->trust_proxy_for_signature(true); - - struct Friend : signalwire::agent::AgentBase { - using signalwire::agent::AgentBase::setup_routes; - using signalwire::agent::AgentBase::init_auth; - }; - auto& f = static_cast(*h.agent); - f.init_auth(); - f.setup_routes(h.srv); - h.start(); - ASSERT_TRUE(h.port > 0); - - // Build a body the SWAIG handler will accept (post_prompt is more - // permissive — pick that endpoint). - std::string body = R"({"call_id":"abc","post_prompt_data":{"parsed":null}})"; - std::string url = "https://example.test/c/post_prompt"; - std::string sig = mw_hmac_sha1_hex(key, url + body); - - httplib::Client cli("127.0.0.1", h.port); - cli.set_basic_auth("u", "p"); - httplib::Headers hdrs = { - {"X-Forwarded-Proto", "https"}, - {"X-Forwarded-Host", "example.test"}, - {"X-SignalWire-Signature", sig}, - }; - auto resp = cli.Post("/c/post_prompt", hdrs, body, "application/json"); - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 200); - return true; + ::unsetenv("SIGNALWIRE_SIGNING_KEY"); + AgentServerHarness h; + h.agent = std::make_shared("c", "/c"); + h.agent->set_auth("u", "p"); + std::string key = "PSKtest-agent-signed-pass"; + h.agent->set_signing_key(key); + // Tell the middleware to honor X-Forwarded-* so we can sign for a + // specific public URL. + h.agent->trust_proxy_for_signature(true); + + struct Friend : signalwire::agent::AgentBase { + using signalwire::agent::AgentBase::init_auth; + using signalwire::agent::AgentBase::setup_routes; + }; + auto& f = static_cast(*h.agent); + f.init_auth(); + f.setup_routes(h.srv); + h.start(); + ASSERT_TRUE(h.port > 0); + + // Build a body the SWAIG handler will accept (post_prompt is more + // permissive — pick that endpoint). + std::string body = R"({"call_id":"abc","post_prompt_data":{"parsed":null}})"; + std::string url = "https://example.test/c/post_prompt"; + std::string sig = mw_hmac_sha1_hex(key, url + body); + + httplib::Client cli("127.0.0.1", h.port); + cli.set_basic_auth("u", "p"); + httplib::Headers hdrs = { + {"X-Forwarded-Proto", "https"}, + {"X-Forwarded-Host", "example.test"}, + {"X-SignalWire-Signature", sig}, + }; + auto resp = cli.Post("/c/post_prompt", hdrs, body, "application/json"); + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 200); + return true; } TEST(webhook_middleware_response_contains_no_signature_or_key_details) { - // Spec: validators MUST NOT log or expose which branch failed, - // which scheme was tried, or what the expected signature was. - // We can at least verify the wire response carries no signature / - // key text. - std::string key = "PSKtest-very-secret-key-string"; - - TestServer ts; - auto downstream = [](const httplib::Request&, httplib::Response& res) { - res.status = 200; - }; - WebhookValidatorOptions opts; - opts.proxy_url_base = "http://localhost.test"; - ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); - ts.start(); - ASSERT_TRUE(ts.port > 0); - - httplib::Client cli("127.0.0.1", ts.port); - httplib::Headers hdrs = {{"X-SignalWire-Signature", "definitely-wrong"}}; - auto resp = cli.Post("/webhook", hdrs, R"({"x":1})", "application/json"); - ASSERT_TRUE((bool)resp); - ASSERT_EQ(resp->status, 403); - ASSERT_TRUE(resp->body.find("PSKtest") == std::string::npos); - ASSERT_TRUE(resp->body.find("definitely-wrong") == std::string::npos); - ASSERT_TRUE(resp->body.find("expected") == std::string::npos); - return true; + // Spec: validators MUST NOT log or expose which branch failed, + // which scheme was tried, or what the expected signature was. + // We can at least verify the wire response carries no signature / + // key text. + std::string key = "PSKtest-very-secret-key-string"; + + TestServer ts; + auto downstream = [](const httplib::Request&, httplib::Response& res) { res.status = 200; }; + WebhookValidatorOptions opts; + opts.proxy_url_base = "http://localhost.test"; + ts.srv.Post("/webhook", WrapWithSignatureValidation(key, downstream, opts)); + ts.start(); + ASSERT_TRUE(ts.port > 0); + + httplib::Client cli("127.0.0.1", ts.port); + httplib::Headers hdrs = {{"X-SignalWire-Signature", "definitely-wrong"}}; + auto resp = cli.Post("/webhook", hdrs, R"({"x":1})", "application/json"); + ASSERT_TRUE((bool)resp); + ASSERT_EQ(resp->status, 403); + ASSERT_TRUE(resp->body.find("PSKtest") == std::string::npos); + ASSERT_TRUE(resp->body.find("definitely-wrong") == std::string::npos); + ASSERT_TRUE(resp->body.find("expected") == std::string::npos); + return true; } diff --git a/tests/test_webhook_validator.cpp b/tests/test_webhook_validator.cpp index 8d46487..937ee29 100644 --- a/tests/test_webhook_validator.cpp +++ b/tests/test_webhook_validator.cpp @@ -8,10 +8,9 @@ // straight from the spec; if they break, this port has a real bug — // DO NOT relax them. -#include "signalwire/security/webhook_validator.hpp" - #include #include + #include #include #include @@ -22,6 +21,8 @@ #include #include +#include "signalwire/security/webhook_validator.hpp" + using namespace signalwire::security; namespace { @@ -32,55 +33,55 @@ namespace { // --------------------------------------------------------------------------- std::string local_b64(const std::string& data) { - static const char table[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - int val = 0, valb = -6; - for (unsigned char c : data) { - val = (val << 8) + c; - valb += 8; - while (valb >= 0) { - out.push_back(table[(val >> valb) & 0x3F]); - valb -= 6; - } + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + int val = 0, valb = -6; + for (unsigned char c : data) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(table[(val >> valb) & 0x3F]); + valb -= 6; } - if (valb > -6) out.push_back(table[((val << 8) >> (valb + 8)) & 0x3F]); - while (out.size() % 4) out.push_back('='); - return out; + } + if (valb > -6) { + out.push_back(table[((val << 8) >> (valb + 8)) & 0x3F]); + } + while (out.size() % 4) { + out.push_back('='); + } + return out; } std::string local_hmac_sha1_b64(const std::string& key, const std::string& msg) { - unsigned char out[EVP_MAX_MD_SIZE]; - unsigned int out_len = 0; - HMAC(EVP_sha1(), - key.data(), static_cast(key.size()), - reinterpret_cast(msg.data()), msg.size(), - out, &out_len); - return local_b64(std::string(reinterpret_cast(out), out_len)); + unsigned char out[EVP_MAX_MD_SIZE]; + unsigned int out_len = 0; + HMAC(EVP_sha1(), key.data(), static_cast(key.size()), + reinterpret_cast(msg.data()), msg.size(), out, &out_len); + return local_b64(std::string(reinterpret_cast(out), out_len)); } std::string local_hmac_sha1_hex(const std::string& key, const std::string& msg) { - unsigned char out[EVP_MAX_MD_SIZE]; - unsigned int out_len = 0; - HMAC(EVP_sha1(), - key.data(), static_cast(key.size()), - reinterpret_cast(msg.data()), msg.size(), - out, &out_len); - std::ostringstream ss; - ss << std::hex << std::setfill('0'); - for (unsigned int i = 0; i < out_len; ++i) { - ss << std::setw(2) << static_cast(out[i]); - } - return ss.str(); + unsigned char out[EVP_MAX_MD_SIZE]; + unsigned int out_len = 0; + HMAC(EVP_sha1(), key.data(), static_cast(key.size()), + reinterpret_cast(msg.data()), msg.size(), out, &out_len); + std::ostringstream ss; + ss << std::hex << std::setfill('0'); + for (unsigned int i = 0; i < out_len; ++i) { + ss << std::setw(2) << static_cast(out[i]); + } + return ss.str(); } // --------------------------------------------------------------------------- // Canonical vectors from porting-sdk/webhooks.md // --------------------------------------------------------------------------- -const char* VEC_A_KEY = "PSKtest1234567890abcdef"; -const char* VEC_A_URL = "https://example.ngrok.io/webhook"; -const char* VEC_A_BODY = R"({"event":"call.state","params":{"call_id":"abc-123","state":"answered"}})"; +const char* VEC_A_KEY = "PSKtest1234567890abcdef"; +const char* VEC_A_URL = "https://example.ngrok.io/webhook"; +const char* VEC_A_BODY = + R"({"event":"call.state","params":{"call_id":"abc-123","state":"answered"}})"; const char* VEC_A_EXPECTED = "c3c08c1fefaf9ee198a100d5906765a6f394bf0f"; const char* VEC_B_KEY = "12345"; @@ -95,7 +96,7 @@ const char* VEC_B_BODY = "&To=%2B18005551212"; const char* VEC_B_EXPECTED = "RSOYDt4T1cUTdK1PDd93/VVr8B8="; -const char* VEC_C_KEY = "PSKtest1234567890abcdef"; +const char* VEC_C_KEY = "PSKtest1234567890abcdef"; const char* VEC_C_BODY = R"({"event":"call.state"})"; const char* VEC_C_URL = "https://example.ngrok.io/webhook?bodySHA256=" @@ -103,46 +104,43 @@ const char* VEC_C_URL = const char* VEC_C_EXPECTED = "dfO9ek8mxyFtn2nMz24plPmPfIY="; FormParams vec_b_params() { - return { - {"CallSid", {"CA1234567890ABCDE"}}, - {"Caller", {"+14158675309"}}, - {"Digits", {"1234"}}, - {"From", {"+14158675309"}}, - {"To", {"+18005551212"}}, - }; + return { + {"CallSid", {"CA1234567890ABCDE"}}, {"Caller", {"+14158675309"}}, {"Digits", {"1234"}}, + {"From", {"+14158675309"}}, {"To", {"+18005551212"}}, + }; } -} // namespace +} // namespace // =========================================================================== // Scheme A — RELAY/JSON (hex) // =========================================================================== TEST(webhook_scheme_a_positive_canonical_vector) { - // Vector A: known JSON body + URL + key produces the known hex digest. - ASSERT_TRUE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, VEC_A_BODY)); - return true; + // Vector A: known JSON body + URL + key produces the known hex digest. + ASSERT_TRUE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, VEC_A_BODY)); + return true; } TEST(webhook_scheme_a_negative_tampered_body) { - // Same key/url, body mutated → returns False. - std::string tampered = VEC_A_BODY; - auto pos = tampered.find("answered"); - ASSERT_TRUE(pos != std::string::npos); - tampered.replace(pos, 8, "ringing!"); // same length, different content - ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, tampered)); - return true; + // Same key/url, body mutated → returns False. + std::string tampered = VEC_A_BODY; + auto pos = tampered.find("answered"); + ASSERT_TRUE(pos != std::string::npos); + tampered.replace(pos, 8, "ringing!"); // same length, different content + ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, tampered)); + return true; } TEST(webhook_scheme_a_negative_wrong_key) { - ASSERT_FALSE(ValidateWebhookSignature("wrong-key", VEC_A_EXPECTED, VEC_A_URL, VEC_A_BODY)); - return true; + ASSERT_FALSE(ValidateWebhookSignature("wrong-key", VEC_A_EXPECTED, VEC_A_URL, VEC_A_BODY)); + return true; } TEST(webhook_scheme_a_negative_wrong_url) { - ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, - "https://example.ngrok.io/different", VEC_A_BODY)); - return true; + ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, VEC_A_EXPECTED, + "https://example.ngrok.io/different", VEC_A_BODY)); + return true; } // =========================================================================== @@ -150,31 +148,30 @@ TEST(webhook_scheme_a_negative_wrong_url) { // =========================================================================== TEST(webhook_scheme_b_positive_canonical_form_vector_via_raw_body) { - // Form params via raw body → matches the canonical Twilio digest. - ASSERT_TRUE(ValidateWebhookSignature(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, VEC_B_BODY)); - return true; + // Form params via raw body → matches the canonical Twilio digest. + ASSERT_TRUE(ValidateWebhookSignature(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, VEC_B_BODY)); + return true; } TEST(webhook_scheme_b_positive_via_validate_request_with_form_params) { - // ValidateRequest(..., FormParams) goes straight to Scheme B with the - // pre-parsed map. - ASSERT_TRUE(ValidateRequest(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, - ParamsOrBody{vec_b_params()})); - return true; + // ValidateRequest(..., FormParams) goes straight to Scheme B with the + // pre-parsed map. + ASSERT_TRUE(ValidateRequest(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, ParamsOrBody{vec_b_params()})); + return true; } TEST(webhook_scheme_b_body_sha256_canonical_vector) { - // Vector C: JSON body on compat surface, signature over URL with bodySHA256. - ASSERT_TRUE(ValidateWebhookSignature(VEC_C_KEY, VEC_C_EXPECTED, VEC_C_URL, VEC_C_BODY)); - return true; + // Vector C: JSON body on compat surface, signature over URL with bodySHA256. + ASSERT_TRUE(ValidateWebhookSignature(VEC_C_KEY, VEC_C_EXPECTED, VEC_C_URL, VEC_C_BODY)); + return true; } TEST(webhook_scheme_b_body_sha256_mismatch_rejected) { - // bodySHA256 in the URL no longer matches the body → reject even though - // the HMAC-over-URL would otherwise pass. - ASSERT_FALSE(ValidateWebhookSignature(VEC_C_KEY, VEC_C_EXPECTED, VEC_C_URL, - R"({"event":"DIFFERENT"})")); - return true; + // bodySHA256 in the URL no longer matches the body → reject even though + // the HMAC-over-URL would otherwise pass. + ASSERT_FALSE( + ValidateWebhookSignature(VEC_C_KEY, VEC_C_EXPECTED, VEC_C_URL, R"({"event":"DIFFERENT"})")); + return true; } // =========================================================================== @@ -182,33 +179,33 @@ TEST(webhook_scheme_b_body_sha256_mismatch_rejected) { // =========================================================================== TEST(webhook_url_with_port_accepted_when_request_has_no_port) { - // Backend signed with :443 — request URL has no port → accept. - std::string key = "test-key"; - std::string url_with_port = "https://example.com:443/webhook"; - std::string url_without_port = "https://example.com/webhook"; - std::string sig = local_hmac_sha1_b64(key, url_with_port); - ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_without_port, "{}")); - return true; + // Backend signed with :443 — request URL has no port → accept. + std::string key = "test-key"; + std::string url_with_port = "https://example.com:443/webhook"; + std::string url_without_port = "https://example.com/webhook"; + std::string sig = local_hmac_sha1_b64(key, url_with_port); + ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_without_port, "{}")); + return true; } TEST(webhook_url_without_port_accepted_when_request_has_standard_port) { - // Backend signed without port — request URL has :443 → accept. - std::string key = "test-key"; - std::string url_with_port = "https://example.com:443/webhook"; - std::string url_without_port = "https://example.com/webhook"; - std::string sig = local_hmac_sha1_b64(key, url_without_port); - ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_with_port, "{}")); - return true; + // Backend signed without port — request URL has :443 → accept. + std::string key = "test-key"; + std::string url_with_port = "https://example.com:443/webhook"; + std::string url_without_port = "https://example.com/webhook"; + std::string sig = local_hmac_sha1_b64(key, url_without_port); + ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_with_port, "{}")); + return true; } TEST(webhook_url_http_port_80_normalization) { - // http + :80 mirrors https + :443. - std::string key = "test-key"; - std::string url_with_port = "http://example.com:80/path"; - std::string url_without_port = "http://example.com/path"; - std::string sig = local_hmac_sha1_b64(key, url_with_port); - ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_without_port, "")); - return true; + // http + :80 mirrors https + :443. + std::string key = "test-key"; + std::string url_with_port = "http://example.com:80/path"; + std::string url_without_port = "http://example.com/path"; + std::string sig = local_hmac_sha1_b64(key, url_with_port); + ASSERT_TRUE(ValidateWebhookSignature(key, sig, url_without_port, "")); + return true; } // =========================================================================== @@ -216,37 +213,37 @@ TEST(webhook_url_http_port_80_normalization) { // =========================================================================== TEST(webhook_repeated_keys_concat_in_submission_order) { - // To=a&To=b → signing string url + "ToaTob". - std::string key = "test-key"; - std::string url = "https://example.com/hook"; - std::string body = "To=a&To=b"; - std::string sig = local_hmac_sha1_b64(key, url + "ToaTob"); - ASSERT_TRUE(ValidateWebhookSignature(key, sig, url, body)); - return true; + // To=a&To=b → signing string url + "ToaTob". + std::string key = "test-key"; + std::string url = "https://example.com/hook"; + std::string body = "To=a&To=b"; + std::string sig = local_hmac_sha1_b64(key, url + "ToaTob"); + ASSERT_TRUE(ValidateWebhookSignature(key, sig, url, body)); + return true; } TEST(webhook_repeated_keys_swapped_order_is_a_different_signature) { - // To=b&To=a is a different submission — rejecting it proves we - // preserve order within repeated keys instead of lexically sorting. - std::string key = "test-key"; - std::string url = "https://example.com/hook"; - std::string sig_for_ab = local_hmac_sha1_b64(key, url + "ToaTob"); - ASSERT_TRUE(ValidateWebhookSignature(key, sig_for_ab, url, "To=a&To=b")); - ASSERT_FALSE(ValidateWebhookSignature(key, sig_for_ab, url, "To=b&To=a")); - return true; + // To=b&To=a is a different submission — rejecting it proves we + // preserve order within repeated keys instead of lexically sorting. + std::string key = "test-key"; + std::string url = "https://example.com/hook"; + std::string sig_for_ab = local_hmac_sha1_b64(key, url + "ToaTob"); + ASSERT_TRUE(ValidateWebhookSignature(key, sig_for_ab, url, "To=a&To=b")); + ASSERT_FALSE(ValidateWebhookSignature(key, sig_for_ab, url, "To=b&To=a")); + return true; } TEST(webhook_repeated_keys_via_form_params_struct) { - // Same idea, exercised through ValidateRequest with FormParams. - std::string key = "test-key"; - std::string url = "https://example.com/hook"; - std::string sig = local_hmac_sha1_b64(key, url + "ToaTob"); - FormParams params = {{"To", {"a", "b"}}}; - ASSERT_TRUE(ValidateRequest(key, sig, url, ParamsOrBody{params})); - // Swap value order under the same key — must NOT match. - FormParams swapped = {{"To", {"b", "a"}}}; - ASSERT_FALSE(ValidateRequest(key, sig, url, ParamsOrBody{swapped})); - return true; + // Same idea, exercised through ValidateRequest with FormParams. + std::string key = "test-key"; + std::string url = "https://example.com/hook"; + std::string sig = local_hmac_sha1_b64(key, url + "ToaTob"); + FormParams params = {{"To", {"a", "b"}}}; + ASSERT_TRUE(ValidateRequest(key, sig, url, ParamsOrBody{params})); + // Swap value order under the same key — must NOT match. + FormParams swapped = {{"To", {"b", "a"}}}; + ASSERT_FALSE(ValidateRequest(key, sig, url, ParamsOrBody{swapped})); + return true; } // =========================================================================== @@ -254,38 +251,34 @@ TEST(webhook_repeated_keys_via_form_params_struct) { // =========================================================================== TEST(webhook_missing_signature_returns_false) { - // Empty signature header → False, no exception. - ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, "", VEC_A_URL, VEC_A_BODY)); - return true; + // Empty signature header → False, no exception. + ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, "", VEC_A_URL, VEC_A_BODY)); + return true; } TEST(webhook_missing_signing_key_throws) { - // Empty signing key → invalid_argument (programming error). - ASSERT_THROWS(ValidateWebhookSignature("", "sig", VEC_A_URL, VEC_A_BODY)); - return true; + // Empty signing key → invalid_argument (programming error). + ASSERT_THROWS(ValidateWebhookSignature("", "sig", VEC_A_URL, VEC_A_BODY)); + return true; } TEST(webhook_missing_signing_key_throws_in_validate_request) { - ASSERT_THROWS(ValidateRequest("", "sig", VEC_A_URL, - ParamsOrBody{std::string{VEC_A_BODY}})); - return true; + ASSERT_THROWS(ValidateRequest("", "sig", VEC_A_URL, ParamsOrBody{std::string{VEC_A_BODY}})); + return true; } TEST(webhook_malformed_signature_returns_false_without_throwing) { - // Wrong length, weird chars, base64 noise — none should throw. - const std::vector garbage = {"xyz", "!!!!", - std::string(100, 'a'), - "%%notbase64%%"}; - for (const auto& g : garbage) { - ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, g, VEC_A_URL, VEC_A_BODY)); - } - return true; + // Wrong length, weird chars, base64 noise — none should throw. + const std::vector garbage = {"xyz", "!!!!", std::string(100, 'a'), "%%notbase64%%"}; + for (const auto& g : garbage) { + ASSERT_FALSE(ValidateWebhookSignature(VEC_A_KEY, g, VEC_A_URL, VEC_A_BODY)); + } + return true; } TEST(webhook_validate_request_missing_signature_returns_false) { - ASSERT_FALSE(ValidateRequest(VEC_B_KEY, "", VEC_B_URL, - ParamsOrBody{vec_b_params()})); - return true; + ASSERT_FALSE(ValidateRequest(VEC_B_KEY, "", VEC_B_URL, ParamsOrBody{vec_b_params()})); + return true; } // =========================================================================== @@ -293,16 +286,15 @@ TEST(webhook_validate_request_missing_signature_returns_false) { // =========================================================================== TEST(webhook_validate_request_with_string_arg_delegates_to_combined) { - // String 4th arg behaves identically to ValidateWebhookSignature. - ASSERT_TRUE(ValidateRequest(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, - ParamsOrBody{std::string{VEC_A_BODY}})); - return true; + // String 4th arg behaves identically to ValidateWebhookSignature. + ASSERT_TRUE( + ValidateRequest(VEC_A_KEY, VEC_A_EXPECTED, VEC_A_URL, ParamsOrBody{std::string{VEC_A_BODY}})); + return true; } TEST(webhook_validate_request_with_map_arg_runs_scheme_b_directly) { - ASSERT_TRUE(ValidateRequest(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, - ParamsOrBody{vec_b_params()})); - return true; + ASSERT_TRUE(ValidateRequest(VEC_B_KEY, VEC_B_EXPECTED, VEC_B_URL, ParamsOrBody{vec_b_params()})); + return true; } // =========================================================================== @@ -311,17 +303,16 @@ TEST(webhook_validate_request_with_map_arg_runs_scheme_b_directly) { // =========================================================================== TEST(webhook_validator_source_uses_crypto_memcmp) { - std::ifstream f(std::string(PROJECT_SOURCE_DIR) + - "/src/security/webhook_validator.cpp"); - ASSERT_TRUE(f.is_open()); - std::stringstream buf; - buf << f.rdbuf(); - std::string src = buf.str(); - // Must use OpenSSL's CRYPTO_memcmp — the porting-sdk webhook spec - // explicitly names it for C++ ports. Plain == on the digest leaks - // the secret over repeated requests. - ASSERT_TRUE(src.find("CRYPTO_memcmp") != std::string::npos); - return true; + std::ifstream f(std::string(PROJECT_SOURCE_DIR) + "/src/security/webhook_validator.cpp"); + ASSERT_TRUE(f.is_open()); + std::stringstream buf; + buf << f.rdbuf(); + std::string src = buf.str(); + // Must use OpenSSL's CRYPTO_memcmp — the porting-sdk webhook spec + // explicitly names it for C++ ports. Plain == on the digest leaks + // the secret over repeated requests. + ASSERT_TRUE(src.find("CRYPTO_memcmp") != std::string::npos); + return true; } // =========================================================================== @@ -329,36 +320,40 @@ TEST(webhook_validator_source_uses_crypto_memcmp) { // =========================================================================== TEST(webhook_scheme_a_recomputed_matches) { - // Independent reconstruction of the Scheme A digest using OpenSSL HMAC. - std::string key = "PSKtest1234567890abcdef"; - std::string url = "https://example.ngrok.io/webhook"; - std::string body = R"({"event":"call.state","params":{"call_id":"abc-123","state":"answered"}})"; - std::string expected = local_hmac_sha1_hex(key, url + body); - ASSERT_TRUE(ValidateWebhookSignature(key, expected, url, body)); - // Sanity: matches the spec value - ASSERT_TRUE(expected == VEC_A_EXPECTED); - return true; + // Independent reconstruction of the Scheme A digest using OpenSSL HMAC. + std::string key = "PSKtest1234567890abcdef"; + std::string url = "https://example.ngrok.io/webhook"; + std::string body = R"({"event":"call.state","params":{"call_id":"abc-123","state":"answered"}})"; + std::string expected = local_hmac_sha1_hex(key, url + body); + ASSERT_TRUE(ValidateWebhookSignature(key, expected, url, body)); + // Sanity: matches the spec value + ASSERT_TRUE(expected == VEC_A_EXPECTED); + return true; } TEST(webhook_scheme_b_form_concat_matches_canonical_signing_string) { - // Independent computation: build the canonical signing string - // url + "CallSidCA...CallerToabc..." and verify it produces the - // canonical base64 digest. - std::string key = "12345"; - std::string url = "https://mycompany.com/myapp.php?foo=1&bar=2"; - std::string concat = - std::string(url) + - "CallSid" "CA1234567890ABCDE" + - "Caller" "+14158675309" + - "Digits" "1234" + - "From" "+14158675309" + - "To" "+18005551212"; - std::string expected = local_hmac_sha1_b64(key, concat); - // Spec value must round-trip - ASSERT_TRUE(expected == VEC_B_EXPECTED); - // And the validator must accept it - ASSERT_TRUE(ValidateRequest(key, expected, url, ParamsOrBody{vec_b_params()})); - return true; + // Independent computation: build the canonical signing string + // url + "CallSidCA...CallerToabc..." and verify it produces the + // canonical base64 digest. + std::string key = "12345"; + std::string url = "https://mycompany.com/myapp.php?foo=1&bar=2"; + std::string concat = std::string(url) + + "CallSid" + "CA1234567890ABCDE" + + "Caller" + "+14158675309" + + "Digits" + "1234" + + "From" + "+14158675309" + + "To" + "+18005551212"; + std::string expected = local_hmac_sha1_b64(key, concat); + // Spec value must round-trip + ASSERT_TRUE(expected == VEC_B_EXPECTED); + // And the validator must accept it + ASSERT_TRUE(ValidateRequest(key, expected, url, ParamsOrBody{vec_b_params()})); + return true; } // =========================================================================== @@ -372,61 +367,61 @@ TEST(webhook_scheme_b_form_concat_matches_canonical_signing_string) { // =========================================================================== TEST(webhook_validate_core_valid_signature_returns_nullopt) { - // Vector A signature in the SignalWire header -> valid -> let it through. - std::map headers{ - {"X-SignalWire-Signature", VEC_A_EXPECTED}, - }; - auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); - ASSERT_FALSE(result.has_value()); // nullopt == pass - return true; + // Vector A signature in the SignalWire header -> valid -> let it through. + std::map headers{ + {"X-SignalWire-Signature", VEC_A_EXPECTED}, + }; + auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); + ASSERT_FALSE(result.has_value()); // nullopt == pass + return true; } TEST(webhook_validate_core_bad_signature_returns_403_triple) { - std::map headers{ - {"X-SignalWire-Signature", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, - }; - auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(std::get<0>(*result), 403); - // No branch-leaking body detail. - ASSERT_EQ(std::get<2>(*result), std::string("Forbidden")); - return true; + std::map headers{ + {"X-SignalWire-Signature", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, + }; + auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(std::get<0>(*result), 403); + // No branch-leaking body detail. + ASSERT_EQ(std::get<2>(*result), std::string("Forbidden")); + return true; } TEST(webhook_validate_core_missing_signature_returns_403_triple) { - std::map headers{}; // no signature header - auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(std::get<0>(*result), 403); - return true; + std::map headers{}; // no signature header + auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(std::get<0>(*result), 403); + return true; } TEST(webhook_validate_core_honors_x_twilio_signature_alias) { - // Same valid Vector A signature, but delivered under the legacy - // X-Twilio-Signature alias — the compat scheme must honor it. - std::map headers{ - {"X-Twilio-Signature", VEC_A_EXPECTED}, - }; - auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); - ASSERT_FALSE(result.has_value()); // nullopt == pass - return true; + // Same valid Vector A signature, but delivered under the legacy + // X-Twilio-Signature alias — the compat scheme must honor it. + std::map headers{ + {"X-Twilio-Signature", VEC_A_EXPECTED}, + }; + auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); + ASSERT_FALSE(result.has_value()); // nullopt == pass + return true; } TEST(webhook_validate_core_header_lookup_is_case_insensitive) { - // Proxies vary header casing; the lookup must be case-insensitive. - std::map headers{ - {"x-signalwire-signature", VEC_A_EXPECTED}, - }; - auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); - ASSERT_FALSE(result.has_value()); - return true; + // Proxies vary header casing; the lookup must be case-insensitive. + std::map headers{ + {"x-signalwire-signature", VEC_A_EXPECTED}, + }; + auto result = Validate("POST", VEC_A_URL, headers, VEC_A_BODY, VEC_A_KEY); + ASSERT_FALSE(result.has_value()); + return true; } TEST(webhook_validate_core_empty_signing_key_throws) { - // Missing signing key is a programming error, not a validation failure. - std::map headers{ - {"X-SignalWire-Signature", VEC_A_EXPECTED}, - }; - ASSERT_THROWS(Validate("POST", VEC_A_URL, headers, VEC_A_BODY, "")); - return true; + // Missing signing key is a programming error, not a validation failure. + std::map headers{ + {"X-SignalWire-Signature", VEC_A_EXPECTED}, + }; + ASSERT_THROWS(Validate("POST", VEC_A_URL, headers, VEC_A_BODY, "")); + return true; } diff --git a/tests/tls_mocktest.cpp b/tests/tls_mocktest.cpp index f776a05..551a0d3 100644 --- a/tests/tls_mocktest.cpp +++ b/tests/tls_mocktest.cpp @@ -4,9 +4,11 @@ // Implementation of the TLS capability-test helpers. See tls_mocktest.hpp. #include "tls_mocktest.hpp" +#include + #include +#include #include -#include #include "httplib.h" @@ -16,108 +18,121 @@ namespace tlstest { namespace { int env_int(const char* name, int fallback) { - if (const char* v = std::getenv(name)) { - if (v && *v) { - try { return std::stoi(v); } catch (...) {} - } + if (const char* v = std::getenv(name)) { + if (v && *v) { + try { + return std::stoi(v); + } catch (const std::exception& e) { + // Falling back is the documented behaviour, but do it VISIBLY: a typo'd + // override that silently behaves as "unset" is how an afternoon is lost. + std::cerr << "warning: " << name << "=\"" << v << "\" is not a number (" << e.what() + << "); using " << fallback << "\n"; + } } - return fallback; + } + return fallback; } // Walk up from PROJECT_SOURCE_DIR (injected by CMake) looking for the adjacent // porting-sdk/test_harness/tls/certs directory. std::string discover_certs_dir() { #ifdef PROJECT_SOURCE_DIR - std::string dir = PROJECT_SOURCE_DIR; + std::string dir = PROJECT_SOURCE_DIR; #else - std::string dir = "."; + std::string dir = "."; #endif - while (true) { - while (dir.size() > 1 && dir.back() == '/') dir.pop_back(); - auto slash = dir.find_last_of('/'); - if (slash == std::string::npos || slash == 0) return std::string(); - std::string parent = dir.substr(0, slash); - std::string candidate = parent + "/porting-sdk/test_harness/tls/certs"; - struct stat st; - if (::stat((candidate + "/ca.crt").c_str(), &st) == 0 && S_ISREG(st.st_mode)) { - return candidate; - } - if (parent == dir) return std::string(); - dir = parent; + while (true) { + while (dir.size() > 1 && dir.back() == '/') { + dir.pop_back(); + } + auto slash = dir.find_last_of('/'); + if (slash == std::string::npos || slash == 0) { + return std::string(); } + std::string parent = dir.substr(0, slash); + std::string candidate = parent + "/porting-sdk/test_harness/tls/certs"; + struct stat st; + if (::stat((candidate + "/ca.crt").c_str(), &st) == 0 && S_ISREG(st.st_mode)) { + return candidate; + } + if (parent == dir) { + return std::string(); + } + dir = parent; + } } // An https:// httplib client that trusts the test CA and verifies the server // certificate (NEVER disabled). Used to drive the REST mock's HTTPS control // plane in TLS mode. httplib::Client tls_control_client(const std::string& base) { - httplib::Client cli(base); - cli.set_connection_timeout(5, 0); - cli.set_read_timeout(10, 0); - std::string ca = ca_cert_path(); - if (!ca.empty()) { - cli.set_ca_cert_path(ca.c_str()); - cli.enable_server_certificate_verification(true); - } - return cli; + httplib::Client cli(base); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(10, 0); + std::string ca = ca_cert_path(); + if (!ca.empty()) { + cli.set_ca_cert_path(ca); + cli.enable_server_certificate_verification(true); + } + return cli; } httplib::Client plain_control_client(const std::string& host, int port) { - httplib::Client cli(host, port); - cli.set_connection_timeout(5, 0); - cli.set_read_timeout(10, 0); - return cli; + httplib::Client cli(host, port); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(10, 0); + return cli; } -} // namespace +} // namespace std::string ca_cert_path() { - static const std::string path = []() { - std::string dir = discover_certs_dir(); - return dir.empty() ? std::string() : dir + "/ca.crt"; - }(); - return path; + static const std::string path = []() { + std::string dir = discover_certs_dir(); + return dir.empty() ? std::string() : dir + "/ca.crt"; + }(); + return path; } std::string trust_test_ca() { - std::string ca = ca_cert_path(); - if (!ca.empty()) ::setenv("SSL_CERT_FILE", ca.c_str(), 1); - return ca; + std::string ca = ca_cert_path(); + if (!ca.empty()) { + ::setenv("SSL_CERT_FILE", ca.c_str(), 1); + } + return ca; } // ---- REST ----------------------------------------------------------------- int rest_tls_port() { return env_int("MOCK_SIGNALWIRE_TLS_PORT", 8773); } -std::string rest_tls_base_url() { - return "https://127.0.0.1:" + std::to_string(rest_tls_port()); -} +std::string rest_tls_base_url() { return "https://127.0.0.1:" + std::to_string(rest_tls_port()); } std::string rest_tls_control_base() { return rest_tls_base_url(); } bool rest_tls_available() { - auto cli = tls_control_client(rest_tls_control_base()); - auto res = cli.Get("/__mock__/health"); - return res && res->status == 200; + auto cli = tls_control_client(rest_tls_control_base()); + auto res = cli.Get("/__mock__/health"); + return res && res->status == 200; } void rest_journal_reset() { - auto cli = tls_control_client(rest_tls_control_base()); - cli.Post("/__mock__/journal/reset", "", "application/json"); - cli.Post("/__mock__/scenarios/reset", "", "application/json"); + auto cli = tls_control_client(rest_tls_control_base()); + cli.Post("/__mock__/journal/reset", "", "application/json"); + cli.Post("/__mock__/scenarios/reset", "", "application/json"); } json rest_journal_last() { - auto cli = tls_control_client(rest_tls_control_base()); - auto res = cli.Get("/__mock__/journal"); - if (!res || res->status != 200) { - throw std::runtime_error("tls_mocktest: REST journal GET failed"); - } - json arr = json::parse(res->body); - if (!arr.is_array() || arr.empty()) { - throw std::runtime_error("tls_mocktest: REST journal empty"); - } - return arr.back(); + auto cli = tls_control_client(rest_tls_control_base()); + auto res = cli.Get("/__mock__/journal"); + if (!res || res->status != 200) { + throw std::runtime_error("tls_mocktest: REST journal GET failed"); + } + json arr = json::parse(res->body); + if (!arr.is_array() || arr.empty()) { + throw std::runtime_error("tls_mocktest: REST journal empty"); + } + return arr.back(); } // ---- RELAY ---------------------------------------------------------------- @@ -125,39 +140,47 @@ json rest_journal_last() { int relay_tls_ws_port() { return env_int("MOCK_RELAY_TLS_PORT", 8783); } int relay_tls_http_port() { - return env_int("MOCK_RELAY_TLS_HTTP_PORT", relay_tls_ws_port() + 1000); + return env_int("MOCK_RELAY_TLS_HTTP_PORT", relay_tls_ws_port() + 1000); } std::string relay_tls_http_url() { - return "http://127.0.0.1:" + std::to_string(relay_tls_http_port()); + return "http://127.0.0.1:" + std::to_string(relay_tls_http_port()); } bool relay_tls_available() { - auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); - auto res = cli.Get("/__mock__/health"); - return res && res->status == 200; + auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); + auto res = cli.Get("/__mock__/health"); + return res && res->status == 200; } void relay_journal_reset() { - auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); - cli.Post("/__mock__/journal/reset", "", "application/json"); - cli.Post("/__mock__/scenarios/reset", "", "application/json"); + auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); + cli.Post("/__mock__/journal/reset", "", "application/json"); + cli.Post("/__mock__/scenarios/reset", "", "application/json"); } std::vector relay_journal_recv(const std::string& method) { - auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); - auto res = cli.Get("/__mock__/journal"); - std::vector out; - if (!res || res->status != 200) return out; - json arr = json::parse(res->body); - if (!arr.is_array()) return out; - for (const auto& e : arr) { - if (e.value("direction", "") != "recv") continue; - if (!method.empty() && e.value("method", "") != method) continue; - out.push_back(e); - } + auto cli = plain_control_client("127.0.0.1", relay_tls_http_port()); + auto res = cli.Get("/__mock__/journal"); + std::vector out; + if (!res || res->status != 200) { return out; + } + json arr = json::parse(res->body); + if (!arr.is_array()) { + return out; + } + for (const auto& e : arr) { + if (e.value("direction", "") != "recv") { + continue; + } + if (!method.empty() && e.value("method", "") != method) { + continue; + } + out.push_back(e); + } + return out; } -} // namespace tlstest -} // namespace signalwire +} // namespace tlstest +} // namespace signalwire diff --git a/tests/tls_mocktest.hpp b/tests/tls_mocktest.hpp index cc656cd..b7b5adb 100644 --- a/tests/tls_mocktest.hpp +++ b/tests/tls_mocktest.hpp @@ -23,9 +23,9 @@ // on infra; CI brings the mocks up so the assertions actually run. #pragma once +#include #include #include -#include namespace signalwire { namespace tlstest { @@ -42,24 +42,24 @@ std::string ca_cert_path(); std::string trust_test_ca(); // ---- REST (mock_signalwire --tls) -------------------------------------- -int rest_tls_port(); // default 8773 -std::string rest_tls_base_url(); // https://127.0.0.1: -std::string rest_tls_control_base(); // https://127.0.0.1: (same host:port) +int rest_tls_port(); // default 8773 +std::string rest_tls_base_url(); // https://127.0.0.1: +std::string rest_tls_control_base(); // https://127.0.0.1: (same host:port) // True iff the https:// REST mock answers /__mock__/health when trusting the // test CA. Performs a real verified GET. bool rest_tls_available(); // Reset + read the REST mock journal over its (HTTPS) control plane. void rest_journal_reset(); -json rest_journal_last(); // throws if empty +json rest_journal_last(); // throws if empty // ---- RELAY (mock_relay --tls) ------------------------------------------ -int relay_tls_ws_port(); // default 8783 -int relay_tls_http_port(); // default 9783 (control plane: HTTP) -std::string relay_tls_http_url(); // http://127.0.0.1: -bool relay_tls_available(); // control-plane /__mock__/health ok +int relay_tls_ws_port(); // default 8783 +int relay_tls_http_port(); // default 9783 (control plane: HTTP) +std::string relay_tls_http_url(); // http://127.0.0.1: +bool relay_tls_available(); // control-plane /__mock__/health ok void relay_journal_reset(); // Inbound (SDK->server) journal frames, optionally filtered by JSON-RPC method. std::vector relay_journal_recv(const std::string& method = ""); -} // namespace tlstest -} // namespace signalwire +} // namespace tlstest +} // namespace signalwire diff --git a/tools/doc_wire_dump.cpp b/tools/doc_wire_dump.cpp index db7c960..056d223 100644 --- a/tools/doc_wire_dump.cpp +++ b/tools/doc_wire_dump.cpp @@ -40,7 +40,9 @@ std::string env_or(const char* name, const std::string& fallback = "") { // explicit URL, fall back to the port on loopback. std::string mock_base_url() { std::string url = env_or("SIGNALWIRE_MOCK_URL"); - if (!url.empty()) return url; + if (!url.empty()) { + return url; + } std::string port = env_or("MOCK_SIGNALWIRE_PORT"); if (port.empty()) { std::cerr << "doc_wire_dump: neither SIGNALWIRE_MOCK_URL nor " @@ -65,66 +67,73 @@ void probe(const char* label, Fn&& fn) { } // namespace int main() { - if (!std::getenv("SIGNALWIRE_LOG_MODE")) { - ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + if (!std::getenv("SIGNALWIRE_LOG_MODE")) { + ::setenv("SIGNALWIRE_LOG_MODE", "off", 1); + } + const std::string base = mock_base_url(); + rest::RestClient client = + rest::RestClient::with_base_url(base, "doc-wire-project", "doc-wire-token"); + + // ---- Phone numbers: search query params (README / rest/README / namespaces) ---- + probe("phone_numbers.search", + [&] { return client.phone_numbers().search({{"areacode", "512"}}); }); + probe("phone_numbers.search+type", [&] { + return client.phone_numbers().search( + {{"areacode", "512"}, {"number_type", "local"}, {"max_results", "3"}}); + }); + // Filter param is the spec's `filter_name` (the SDK forwards query keys + // verbatim, so a bare `name` would land off-spec — see DOC-WIRE §2.1). + probe("phone_numbers.list", + [&] { return client.phone_numbers().list({{"filter_name", "Main"}}); }); + + // ---- Fabric AI agents: create body (rest_manage_resources / api_reference) ---- + probe("fabric.ai_agents.create", [&] { + return client.fabric().ai_agents.create( + {{"name", "Demo Support Bot"}, + {"prompt", {{"text", "You are a friendly support agent."}}}}); + }); + probe("fabric.ai_agents.list", [&] { return client.fabric().ai_agents.list(); }); + + // ---- Calling: dial body (README quickstart / rest examples) ---- + probe("calling.dial", [&] { + rest::generated::Calling::DialParams p; + p.from = "+15559876543"; + p.to = "+15551234567"; + p.url = "https://example.com/handler"; + return client.calling().dial(p); + }); + + // ---- Datasphere: document search body (README / namespaces) ---- + probe("datasphere.documents.search", [&] { + rest::generated::DatasphereDocuments::SearchParams p; + p.query_string = "billing policy"; + p.count = 5; + return client.datasphere().documents.search(p); + }); + probe("datasphere.documents.create", [&] { + return client.datasphere().documents.create( + {{"url", "https://example.com/doc.pdf"}, {"tags", {"support"}}}); + }); + + // ---- Video rooms: create body (namespaces) ---- + probe("video.rooms.create", + [&] { return client.video().rooms.create({{"name", "standup"}, {"max_members", 10}}); }); + + // ---- Queues: create body (namespaces) ---- + probe("queues.create", [&] { return client.queues().create({{"name", "Support"}}); }); + + // ---- Registry brands: create body (namespaces 10DLC) ---- + probe("registry.brands.create", [&] { + return client.registry().brands.create({{"name", "My Brand"}, {"ein", "12-3456789"}}); + }); + + std::cout << "doc_wire_dump: replayed documented REST call shapes against " << base << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; } - const std::string base = mock_base_url(); - rest::RestClient client = - rest::RestClient::with_base_url(base, "doc-wire-project", "doc-wire-token"); - - // ---- Phone numbers: search query params (README / rest/README / namespaces) ---- - probe("phone_numbers.search", - [&] { return client.phone_numbers().search({{"areacode", "512"}}); }); - probe("phone_numbers.search+type", [&] { - return client.phone_numbers().search( - {{"areacode", "512"}, {"number_type", "local"}, {"max_results", "3"}}); - }); - // Filter param is the spec's `filter_name` (the SDK forwards query keys - // verbatim, so a bare `name` would land off-spec — see DOC-WIRE §2.1). - probe("phone_numbers.list", - [&] { return client.phone_numbers().list({{"filter_name", "Main"}}); }); - - // ---- Fabric AI agents: create body (rest_manage_resources / api_reference) ---- - probe("fabric.ai_agents.create", [&] { - return client.fabric().ai_agents.create( - {{"name", "Demo Support Bot"}, - {"prompt", {{"text", "You are a friendly support agent."}}}}); - }); - probe("fabric.ai_agents.list", [&] { return client.fabric().ai_agents.list(); }); - - // ---- Calling: dial body (README quickstart / rest examples) ---- - probe("calling.dial", [&] { - rest::generated::Calling::DialParams p; - p.from = "+15559876543"; - p.to = "+15551234567"; - p.url = "https://example.com/handler"; - return client.calling().dial(p); - }); - - // ---- Datasphere: document search body (README / namespaces) ---- - probe("datasphere.documents.search", [&] { - rest::generated::DatasphereDocuments::SearchParams p; - p.query_string = "billing policy"; - p.count = 5; - return client.datasphere().documents.search(p); - }); - probe("datasphere.documents.create", [&] { - return client.datasphere().documents.create( - {{"url", "https://example.com/doc.pdf"}, {"tags", {"support"}}}); - }); - - // ---- Video rooms: create body (namespaces) ---- - probe("video.rooms.create", - [&] { return client.video().rooms.create({{"name", "standup"}, {"max_members", 10}}); }); - - // ---- Queues: create body (namespaces) ---- - probe("queues.create", [&] { return client.queues().create({{"name", "Support"}}); }); - - // ---- Registry brands: create body (namespaces 10DLC) ---- - probe("registry.brands.create", [&] { - return client.registry().brands.create({{"name", "My Brand"}, {"ein", "12-3456789"}}); - }); - - std::cout << "doc_wire_dump: replayed documented REST call shapes against " << base << "\n"; - return 0; } diff --git a/tools/emit_corpus.cpp b/tools/emit_corpus.cpp index 4d62893..4bbb57c 100644 --- a/tools/emit_corpus.cpp +++ b/tools/emit_corpus.cpp @@ -351,22 +351,29 @@ std::vector corpus() { } // namespace int main() { - json out = json::object(); - std::vector seen; - for (auto& e : corpus()) { - for (const auto& s : seen) { - if (s == e.id) { - std::cerr << "emit_corpus: duplicate corpus id " << e.id << "\n"; - return 1; + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); + std::vector seen; + for (auto& e : corpus()) { + for (const auto& s : seen) { + if (s == e.id) { + std::cerr << "emit_corpus: duplicate corpus id " << e.id << "\n"; + return 1; + } } + seen.push_back(e.id); + out[e.id] = e.build().to_json(); } - seen.push_back(e.id); - out[e.id] = e.build().to_json(); - } - // nlohmann sorts object keys -> canonical JSON. dump() without indent for a - // single compact line; ensure_ascii is off by default so '+'/'&' stay literal - // (matches Python's json output, like Go's SetEscapeHTML(false)). - std::cout << out.dump() << "\n"; - return 0; + // nlohmann sorts object keys -> canonical JSON. dump() without indent for a + // single compact line; ensure_ascii is off by default so '+'/'&' stay literal + // (matches Python's json output, like Go's SetEscapeHTML(false)). + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/envelope_dump.cpp b/tools/envelope_dump.cpp index 5eb9296..1aa4d35 100644 --- a/tools/envelope_dump.cpp +++ b/tools/envelope_dump.cpp @@ -320,86 +320,93 @@ std::vector corpus() { } // namespace int main() { - json out = json::object(); - - for (const auto& c : corpus()) { - json artifact = { - {"raised", false}, {"error_kind", nullptr}, - {"status_code", nullptr}, {"body_error_code", nullptr}, - {"request_count", 0}, - }; - - RestClient client = [&]() -> RestClient { - if (c.transport) { - // Point at a DEAD port -- nothing listening once released. No mock - // harness involved (a fresh, disposable RestClient), so no journal - // scoping applies; request_count stays 0 by construction. - int port = dead_port(); - return RestClient::with_base_url("http://127.0.0.1:" + std::to_string(port), - "envelope_proj", "envelope_tok"); - } - // A fresh mock-backed client per case: unique random project -> unique - // auth header -> an isolated (empty-start) scoped journal view, so - // request_count is exact without needing an explicit reset. - auto mc = signalwire::rest::mocktest::make_client(); - if (c.has_scenario) { - // Arm the SAME override scenario_repeat times (FIFO), so a retry-armed - // case sees the failure on every attempt it is armed for. - for (int i = 0; i < c.scenario_repeat; ++i) { - signalwire::rest::mocktest::scenario_set(c.endpoint, c.status, c.response); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); + + for (const auto& c : corpus()) { + json artifact = { + {"raised", false}, {"error_kind", nullptr}, + {"status_code", nullptr}, {"body_error_code", nullptr}, + {"request_count", 0}, + }; + + RestClient client = [&]() -> RestClient { + if (c.transport) { + // Point at a DEAD port -- nothing listening once released. No mock + // harness involved (a fresh, disposable RestClient), so no journal + // scoping applies; request_count stays 0 by construction. + int port = dead_port(); + return RestClient::with_base_url("http://127.0.0.1:" + std::to_string(port), + "envelope_proj", "envelope_tok"); + } + // A fresh mock-backed client per case: unique random project -> unique + // auth header -> an isolated (empty-start) scoped journal view, so + // request_count is exact without needing an explicit reset. + auto mc = signalwire::rest::mocktest::make_client(); + if (c.has_scenario) { + // Arm the SAME override scenario_repeat times (FIFO), so a retry-armed + // case sees the failure on every attempt it is armed for. + for (int i = 0; i < c.scenario_repeat; ++i) { + signalwire::rest::mocktest::scenario_set(c.endpoint, c.status, c.response); + } } + return mc; + }(); + + // Build the per-request options envelope. + RequestOptions ro; + if (c.has_request_options) { + ro.retries = c.retries; + ro.retry_backoff = c.retry_backoff; } - return mc; - }(); - - // Build the per-request options envelope. - RequestOptions ro; - if (c.has_request_options) { - ro.retries = c.retries; - ro.retry_backoff = c.retry_backoff; - } - try { - json body; - if (c.is_post) { - body = client.http_client().post(c.call_path, c.post_body, ro); - } else { - body = client.http_client().get(c.call_path, {}, ro); + try { + json body; + if (c.is_post) { + body = client.http_client().post(c.call_path, c.post_body, ro); + } else { + body = client.http_client().get(c.call_path, {}, ro); + } + (void)body; + } catch (const SignalWireRestError& e) { + // A member of the typed error family (HTTP error OR transport error). + artifact["raised"] = true; + artifact["error_kind"] = "typed"; + // status_code() == 0 => a transport failure (no HTTP response); report null + // so the artifact matches the oracle (python raises status_code=None). + artifact["status_code"] = e.status_code() == 0 ? json(nullptr) : json(e.status_code()); + artifact["body_error_code"] = decode_body_error_code(e.body()); + } catch (const std::exception& e) { + // A leaked, non-family exception -- the contract violation the gate + // catches. + artifact["raised"] = true; + artifact["error_kind"] = std::string("bare:") + typeid(e).name(); } - (void)body; - } catch (const SignalWireRestError& e) { - // A member of the typed error family (HTTP error OR transport error). - artifact["raised"] = true; - artifact["error_kind"] = "typed"; - // status_code() == 0 => a transport failure (no HTTP response); report null - // so the artifact matches the oracle (python raises status_code=None). - artifact["status_code"] = e.status_code() == 0 ? json(nullptr) : json(e.status_code()); - artifact["body_error_code"] = decode_body_error_code(e.body()); - } catch (const std::exception& e) { - // A leaked, non-family exception -- the contract violation the gate - // catches. - artifact["raised"] = true; - artifact["error_kind"] = std::string("bare:") + typeid(e).name(); - } - if (!c.transport) { - // Count journal hits for the path (retry check: 1 == no retry, retries+1 - // for a retry-armed case). Scoped to this case's client via the - // thread-local active scope make_client() set, so a concurrent run can't - // cross-contaminate. - int count = 0; - for (const auto& j : signalwire::rest::mocktest::journal()) { - if (j.path == c.call_path) { - ++count; + if (!c.transport) { + // Count journal hits for the path (retry check: 1 == no retry, retries+1 + // for a retry-armed case). Scoped to this case's client via the + // thread-local active scope make_client() set, so a concurrent run can't + // cross-contaminate. + int count = 0; + for (const auto& j : signalwire::rest::mocktest::journal()) { + if (j.path == c.call_path) { + ++count; + } } + artifact["request_count"] = count; + signalwire::rest::mocktest::clear_active_scope(); } - artifact["request_count"] = count; - signalwire::rest::mocktest::clear_active_scope(); + + out[c.id] = artifact; } - out[c.id] = artifact; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; } - - std::cout << out.dump() << "\n"; - return 0; } diff --git a/tools/http_dump.cpp b/tools/http_dump.cpp index a977bc9..926c3ce 100644 --- a/tools/http_dump.cpp +++ b/tools/http_dump.cpp @@ -74,10 +74,17 @@ json observe_response(int status, const std::map& head json out = json::object(); out["status"] = status; std::vector keys; - for (const auto& [k, v] : headers) keys.push_back(k); + keys.reserve(headers.size()); + for (const auto& [k, _v] : headers) { + keys.push_back(k); + } out["header_keys"] = keys; // std::map already sorted - if (headers.count("Location")) out["location"] = headers.at("Location"); - if (headers.count("WWW-Authenticate")) out["www_authenticate"] = headers.at("WWW-Authenticate"); + if (headers.count("Location")) { + out["location"] = headers.at("Location"); + } + if (headers.count("WWW-Authenticate")) { + out["www_authenticate"] = headers.at("WWW-Authenticate"); + } if (kind == "response_full") { if (body_str.empty()) { out["body"] = ""; @@ -147,110 +154,148 @@ json reduce_serverless(const signalwire::utils::ServerlessResponse& res) { } // namespace int main() { - json out = json::object(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); - // ---- handle_request: 200 SWML happy path ---- - { - Service svc; - init_service(svc); - auto [status, headers, body] = - svc.handle_request("GET", "http://localhost:3000/swml", - {{"Authorization", basic_auth(kUser, kPassword)}}, std::nullopt); - out["http_handle_request_200_swml"] = observe_response(status, headers, body, "response_full"); - } - // ---- handle_request: 401 no auth ---- - { - Service svc; - init_service(svc); - auto [status, headers, body] = - svc.handle_request("GET", "http://localhost:3000/swml", {}, std::nullopt); - out["http_handle_request_401_no_auth"] = - observe_response(status, headers, body, "response_full"); - } - // ---- handle_request: 401 bad password (status+headers only) ---- - { - Service svc; - init_service(svc); - auto [status, headers, body] = - svc.handle_request("GET", "http://localhost:3000/swml", - {{"Authorization", basic_auth(kUser, "wrong")}}, std::nullopt); - out["http_handle_request_401_bad_password"] = - observe_response(status, headers, body, "response_status_headers"); - } - // ---- handle_request: 307 redirect via routing callback ---- - { - Service svc; - init_service(svc); - svc.register_routing_callback(redirect_cb, "/sip"); - json body = json{{"call", {{"to", "sip:redirect-me@space"}}}}; - auto [status, headers, body_str] = - svc.handle_request("POST", "http://localhost:3000/swml/sip", - {{"Authorization", basic_auth(kUser, kPassword)}}, body); - out["http_handle_request_307_redirect"] = - observe_response(status, headers, body_str, "response_full"); - } - // ---- handle_request: callback returns "" -> normal 200 SWML ---- - { - Service svc; - init_service(svc); - svc.register_routing_callback(redirect_cb, "/sip"); - json body = json{{"call", {{"to", "sip:keep@space"}}}}; - auto [status, headers, body_str] = - svc.handle_request("POST", "http://localhost:3000/swml/sip", - {{"Authorization", basic_auth(kUser, kPassword)}}, body); - out["http_handle_request_callback_passthrough_200"] = - observe_response(status, headers, body_str, "response_full"); - } + // ---- handle_request: 200 SWML happy path ---- + { + Service svc; + init_service(svc); + auto [status, headers, body] = + svc.handle_request("GET", "http://localhost:3000/swml", + {{"Authorization", basic_auth(kUser, kPassword)}}, std::nullopt); + out["http_handle_request_200_swml"] = + observe_response(status, headers, body, "response_full"); + } + // ---- handle_request: 401 no auth ---- + { + Service svc; + init_service(svc); + auto [status, headers, body] = + svc.handle_request("GET", "http://localhost:3000/swml", {}, std::nullopt); + out["http_handle_request_401_no_auth"] = + observe_response(status, headers, body, "response_full"); + } + // ---- handle_request: 401 bad password (status+headers only) ---- + { + Service svc; + init_service(svc); + auto [status, headers, body] = + svc.handle_request("GET", "http://localhost:3000/swml", + {{"Authorization", basic_auth(kUser, "wrong")}}, std::nullopt); + out["http_handle_request_401_bad_password"] = + observe_response(status, headers, body, "response_status_headers"); + } + // ---- handle_request: 307 redirect via routing callback ---- + { + Service svc; + init_service(svc); + svc.register_routing_callback(redirect_cb, "/sip"); + json body = json{{"call", {{"to", "sip:redirect-me@space"}}}}; + auto [status, headers, body_str] = + svc.handle_request("POST", "http://localhost:3000/swml/sip", + {{"Authorization", basic_auth(kUser, kPassword)}}, body); + out["http_handle_request_307_redirect"] = + observe_response(status, headers, body_str, "response_full"); + } + // ---- handle_request: callback returns "" -> normal 200 SWML ---- + { + Service svc; + init_service(svc); + svc.register_routing_callback(redirect_cb, "/sip"); + json body = json{{"call", {{"to", "sip:keep@space"}}}}; + auto [status, headers, body_str] = + svc.handle_request("POST", "http://localhost:3000/swml/sip", + {{"Authorization", basic_auth(kUser, kPassword)}}, body); + out["http_handle_request_callback_passthrough_200"] = + observe_response(status, headers, body_str, "response_full"); + } - // ---- extract_sip_username: pure extractor ---- - out["http_extract_sip_username_sip"] = - extract_username(json{{"call", {{"to", "sip:alice@agents.signalwire.com"}}}}); - out["http_extract_sip_username_tel"] = - extract_username(json{{"call", {{"to", "tel:+15551234567"}}}}); - out["http_extract_sip_username_plain"] = extract_username(json{{"call", {{"to", "support"}}}}); - out["http_extract_sip_username_missing"] = extract_username(json{{"vars", json::object()}}); + // ---- extract_sip_username: pure extractor ---- + out["http_extract_sip_username_sip"] = + extract_username(json{{"call", {{"to", "sip:alice@agents.signalwire.com"}}}}); + out["http_extract_sip_username_tel"] = + extract_username(json{{"call", {{"to", "tel:+15551234567"}}}}); + out["http_extract_sip_username_plain"] = extract_username(json{{"call", {{"to", "support"}}}}); + out["http_extract_sip_username_missing"] = extract_username(json{{"vars", json::object()}}); - // ---- webhook validate ---- - out["http_webhook_validate_ok"] = webhook_decision( - "POST", kWhUrl, kWhBody, - {{"x-signalwire-signature", webhook_sig(kWhUrl, kWhBody, kSigningKey)}}, kSigningKey); - { - std::string bad; - for (int i = 0; i < 5; ++i) bad += "deadbeef"; - out["http_webhook_validate_bad_sig"] = - webhook_decision("POST", kWhUrl, kWhBody, {{"x-signalwire-signature", bad}}, kSigningKey); - } - out["http_webhook_validate_missing_sig"] = - webhook_decision("POST", kWhUrl, kWhBody, {}, kSigningKey); - out["http_webhook_validate_twilio_alias"] = webhook_decision( - "POST", kWhUrl, kWhBody, {{"x-twilio-signature", webhook_sig(kWhUrl, kWhBody, kSigningKey)}}, - kSigningKey); + // ---- webhook validate ---- + out["http_webhook_validate_ok"] = webhook_decision( + "POST", kWhUrl, kWhBody, + {{"x-signalwire-signature", webhook_sig(kWhUrl, kWhBody, kSigningKey)}}, kSigningKey); + { + std::string bad; + for (int i = 0; i < 5; ++i) { + bad += "deadbeef"; + } + out["http_webhook_validate_bad_sig"] = + webhook_decision("POST", kWhUrl, kWhBody, {{"x-signalwire-signature", bad}}, kSigningKey); + } + out["http_webhook_validate_missing_sig"] = + webhook_decision("POST", kWhUrl, kWhBody, {}, kSigningKey); + out["http_webhook_validate_twilio_alias"] = webhook_decision( + "POST", kWhUrl, kWhBody, + {{"x-twilio-signature", webhook_sig(kWhUrl, kWhBody, kSigningKey)}}, kSigningKey); - // ---- serverless (lambda) ---- - { - // SWAIG dispatch. Matches the oracle ctor (name "demo", route "/demo"). - AgentBase a("demo", "/demo"); - a.set_auth(kUser, kPassword); - signalwire::swaig::ToolHandler handler = [](const json&, const json&) { - return FunctionResult("hello there"); - }; - a.define_tool("say_hello", "greet", json::object(), handler); - json event = { - {"rawPath", "/swaig"}, - {"headers", - {{"authorization", basic_auth(kUser, kPassword)}, {"content-type", "application/json"}}}, - {"body", R"({"function":"say_hello","argument":{"parsed":[{}]},"call_id":"c1"})"}}; - auto res = signalwire::utils::handle_lambda(a, event); - out["http_serverless_lambda_swaig"] = reduce_serverless(res); - } - { - AgentBase a("demo", "/demo"); - a.set_auth(kUser, kPassword); - json event = {{"rawPath", "/"}, {"headers", json::object()}, {"body", nullptr}}; - auto res = signalwire::utils::handle_lambda(a, event); - out["http_serverless_lambda_noauth_401"] = reduce_serverless(res); - } + // ---- serverless (lambda) ---- + { + // SWAIG dispatch. Matches the oracle ctor (name "demo", route "/demo"). + AgentBase a("demo", "/demo"); + a.set_auth(kUser, kPassword); + signalwire::swaig::ToolHandler handler = [](const json&, const json&) { + return FunctionResult("hello there"); + }; + a.define_tool("say_hello", "greet", json::object(), handler); + json event = { + {"rawPath", "/swaig"}, + {"headers", + {{"authorization", basic_auth(kUser, kPassword)}, {"content-type", "application/json"}}}, + {"body", R"({"function":"say_hello","argument":{"parsed":[{}]},"call_id":"c1"})"}}; + auto res = signalwire::utils::handle_lambda(a, event); + out["http_serverless_lambda_swaig"] = reduce_serverless(res); + } + { + // The POSITIVE half of the serverless token contract: identical to the + // fixture above in every respect EXCEPT that it carries a GENUINELY MINTED + // token in the lambda query string, so it pins that a valid credential is + // ACCEPTED and the secure tool RUNS. Without it the contract would only + // ever be proven in the refusing direction, and an implementation that + // refused EVERYTHING would sail through. + // + // The token cannot be a corpus literal: it is an HMAC keyed by this + // agent's per-process random secret and it expires. It is minted HERE, + // from the SAME agent instance the fixture drives, exactly as the oracle + // mints its own. + AgentBase a("demo", "/demo"); + a.set_auth(kUser, kPassword); + signalwire::swaig::ToolHandler handler = [](const json&, const json&) { + return FunctionResult("hello there"); + }; + a.define_tool("say_hello", "greet", json::object(), handler); + const std::string token = a.create_tool_token("say_hello", "c1"); + json event = { + {"rawPath", "/swaig"}, + {"headers", + {{"authorization", basic_auth(kUser, kPassword)}, {"content-type", "application/json"}}}, + {"queryStringParameters", {{"__token", token}}}, + {"body", R"({"function":"say_hello","argument":{"parsed":[{}]},"call_id":"c1"})"}}; + auto res = signalwire::utils::handle_lambda(a, event); + out["http_serverless_lambda_swaig_valid_token"] = reduce_serverless(res); + } + { + AgentBase a("demo", "/demo"); + a.set_auth(kUser, kPassword); + json event = {{"rawPath", "/"}, {"headers", json::object()}, {"body", nullptr}}; + auto res = signalwire::utils::handle_lambda(a, event); + out["http_serverless_lambda_noauth_401"] = reduce_serverless(res); + } - std::cout << out.dump() << "\n"; - return 0; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/live_smoke.cpp b/tools/live_smoke.cpp index 283c845..e1e769a 100644 --- a/tools/live_smoke.cpp +++ b/tools/live_smoke.cpp @@ -41,59 +41,66 @@ bool env_set(const char* key) { } // namespace int main() { - const char* opt_in = std::getenv("SWSDK_LIVE_TESTS"); - if (opt_in == nullptr || std::string(opt_in) != "1") { - return skip("SWSDK_LIVE_TESTS not set"); - } - if (!env_set("SIGNALWIRE_PROJECT_ID") || !env_set("SIGNALWIRE_API_TOKEN") || - !env_set("SIGNALWIRE_SPACE")) { - return skip("real credentials absent"); - } - - // 1. auth + 2. REST read — construct the client and make one real GET. + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. try { - auto rest = signalwire::rest::RestClient::from_env(); - std::cerr << "live_smoke: [1/4] REST client constructed\n"; - auto result = rest.phone_numbers().search({{"areacode", "212"}}); - std::size_t rows = 0; - if (result.contains("data") && result["data"].is_array()) { - rows = result["data"].size(); + const char* opt_in = std::getenv("SWSDK_LIVE_TESTS"); + if (opt_in == nullptr || std::string(opt_in) != "1") { + return skip("SWSDK_LIVE_TESTS not set"); + } + if (!env_set("SIGNALWIRE_PROJECT_ID") || !env_set("SIGNALWIRE_API_TOKEN") || + !env_set("SIGNALWIRE_SPACE")) { + return skip("real credentials absent"); } - std::cerr << "live_smoke: [2/4] REST read ok (" << rows << " rows)\n"; - } catch (const std::exception& e) { - std::cerr << "live_smoke: FAIL — REST read: " << e.what() << "\n"; - return 1; - } - // 3. SWML render — no network; proves document generation end to end. - try { - signalwire::agent::AgentBase agent("live-smoke", "/agent"); - agent.prompt_add_section("Role", "You are a smoke test."); - auto swml = agent.render_swml(); - if (!swml.contains("version")) { - std::cerr << "live_smoke: FAIL — SWML render produced no version\n"; + // 1. auth + 2. REST read — construct the client and make one real GET. + try { + auto rest = signalwire::rest::RestClient::from_env(); + std::cerr << "live_smoke: [1/4] REST client constructed\n"; + auto result = rest.phone_numbers().search({{"areacode", "212"}}); + std::size_t rows = 0; + if (result.contains("data") && result["data"].is_array()) { + rows = result["data"].size(); + } + std::cerr << "live_smoke: [2/4] REST read ok (" << rows << " rows)\n"; + } catch (const std::exception& e) { + std::cerr << "live_smoke: FAIL — REST read: " << e.what() << "\n"; return 1; } - std::cerr << "live_smoke: [3/4] SWML render ok\n"; - } catch (const std::exception& e) { - std::cerr << "live_smoke: FAIL — SWML render: " << e.what() << "\n"; - return 1; - } - // 4. RELAY connect — open + close a WS session against the real platform. - try { - auto relay = signalwire::relay::RelayClient::from_env(); - if (!relay.connect()) { - std::cerr << "live_smoke: FAIL — RELAY connect returned false\n"; + // 3. SWML render — no network; proves document generation end to end. + try { + signalwire::agent::AgentBase agent("live-smoke", "/agent"); + agent.prompt_add_section("Role", "You are a smoke test."); + auto swml = agent.render_swml(); + if (!swml.contains("version")) { + std::cerr << "live_smoke: FAIL — SWML render produced no version\n"; + return 1; + } + std::cerr << "live_smoke: [3/4] SWML render ok\n"; + } catch (const std::exception& e) { + std::cerr << "live_smoke: FAIL — SWML render: " << e.what() << "\n"; return 1; } - relay.disconnect(); - std::cerr << "live_smoke: [4/4] RELAY connect ok\n"; + + // 4. RELAY connect — open + close a WS session against the real platform. + try { + auto relay = signalwire::relay::RelayClient::from_env(); + if (!relay.connect()) { + std::cerr << "live_smoke: FAIL — RELAY connect returned false\n"; + return 1; + } + relay.disconnect(); + std::cerr << "live_smoke: [4/4] RELAY connect ok\n"; + } catch (const std::exception& e) { + std::cerr << "live_smoke: FAIL — RELAY connect: " << e.what() << "\n"; + return 1; + } + + std::cout << "live_smoke: PASS (auth + REST read + SWML render + RELAY connect)\n"; + return 0; } catch (const std::exception& e) { - std::cerr << "live_smoke: FAIL — RELAY connect: " << e.what() << "\n"; + std::cerr << "fatal: " << e.what() << "\n"; return 1; } - - std::cout << "live_smoke: PASS (auth + REST read + SWML render + RELAY connect)\n"; - return 0; } diff --git a/tools/pagination_dump.cpp b/tools/pagination_dump.cpp index a096fd2..9bbe4c4 100644 --- a/tools/pagination_dump.cpp +++ b/tools/pagination_dump.cpp @@ -225,11 +225,18 @@ json classify(const Fixture& f) { } // namespace int main() { - signalwire::Logger::instance().suppress(); - json out = json::object(); - for (const auto& f : corpus()) { - out[f.id] = classify(f); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + signalwire::Logger::instance().suppress(); + json out = json::object(); + for (const auto& f : corpus()) { + out[f.id] = classify(f); + } + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; } - std::cout << out.dump() << "\n"; - return 0; } diff --git a/tools/relay_liveness_dump.cpp b/tools/relay_liveness_dump.cpp index 54dc7c8..edd1db7 100644 --- a/tools/relay_liveness_dump.cpp +++ b/tools/relay_liveness_dump.cpp @@ -394,8 +394,11 @@ json drive_reconnect() { auto tp = Clock::now(); try { (void)c.execute("calling.play", play_params()); + // An error here is FINE and expected -- this probe measures only whether the + // call returns within a bounded window, not whether it succeeds. Swallowing + // is the intent; the elapsed time below is the assertion. + // NOLINTNEXTLINE(bugprone-empty-catch) } catch (const std::exception&) { - // an error is fine; boundedness is what matters } out["pending_faulted_not_hung"] = std::chrono::duration_cast(Clock::now() - tp).count() < @@ -449,23 +452,30 @@ json drive_max_active_calls(int cap) { } // namespace int main() { - if (!std::getenv("RELAY_DUMP_DEBUG")) { - signalwire::Logger::instance().suppress(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + if (!std::getenv("RELAY_DUMP_DEBUG")) { + signalwire::Logger::instance().suppress(); + } + ix::initNetSystem(); + + json out = json::object(); + out["cred_missing_project"] = drive_cred_missing("project"); + out["cred_missing_token"] = drive_cred_missing("token"); + out["cred_auth_reject"] = drive_cred_auth_reject(); + out["relay_contract_500"] = drive_relay_contract("500"); + out["relay_contract_404"] = drive_relay_contract("404"); + out["relay_contract_410"] = drive_relay_contract("410"); + out["dead_peer_half_open"] = drive_dead_peer(); + out["black_hole_silent_peer"] = drive_black_hole(); + out["reconnect_after_drop"] = drive_reconnect(); + out["max_active_calls_cap"] = drive_max_active_calls(2); + + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; } - ix::initNetSystem(); - - json out = json::object(); - out["cred_missing_project"] = drive_cred_missing("project"); - out["cred_missing_token"] = drive_cred_missing("token"); - out["cred_auth_reject"] = drive_cred_auth_reject(); - out["relay_contract_500"] = drive_relay_contract("500"); - out["relay_contract_404"] = drive_relay_contract("404"); - out["relay_contract_410"] = drive_relay_contract("410"); - out["dead_peer_half_open"] = drive_dead_peer(); - out["black_hole_silent_peer"] = drive_black_hole(); - out["reconnect_after_drop"] = drive_reconnect(); - out["max_active_calls_cap"] = drive_max_active_calls(2); - - std::cout << out.dump() << "\n"; - return 0; } diff --git a/tools/route_registry.cpp b/tools/route_registry.cpp index 1ec6fd8..8649b41 100644 --- a/tools/route_registry.cpp +++ b/tools/route_registry.cpp @@ -67,13 +67,22 @@ class CaptureServer { public: CaptureServer() { auto handler = [this](const std::string& method) { - return [this, method](const httplib::Request& req, httplib::Response& res) { - { - std::lock_guard lock(mutex_); - captured_.emplace_back(method, req.path); + // The body is wrapped in try/catch(...) below, so nothing actually + // escapes into httplib's worker thread; clang-tidy cannot see through the + // lambda boundary, hence the inline suppression on the capture list. + // Same shape and rationale as src/web/web_service.cpp:205. + return [this, method]( // NOLINT(bugprone-exception-escape) + const httplib::Request& req, httplib::Response& res) { + try { + { + std::lock_guard lock(mutex_); + captured_.emplace_back(method, req.path); + } + res.status = 200; + res.set_content("{}", "application/json"); + } catch (...) { + res.status = 500; } - res.status = 200; - res.set_content("{}", "application/json"); }; }; server_.Get(".*", handler("GET")); @@ -90,7 +99,9 @@ class CaptureServer { } ~CaptureServer() { server_.stop(); - if (thread_.joinable()) thread_.join(); + if (thread_.joinable()) { + thread_.join(); + } } std::string base_url() const { return "http://127.0.0.1:" + std::to_string(port_); } @@ -124,7 +135,9 @@ std::string templatize(const std::string& path) { std::string seg = (slash == std::string::npos) ? p.substr(start) : p.substr(start, slash - start); out += (seg == SENTINEL) ? "{id}" : seg; - if (slash == std::string::npos) break; + if (slash == std::string::npos) { + break; + } out += "/"; start = slash + 1; } @@ -518,44 +531,51 @@ std::vector> invoke_all(RestClient& c) { } // namespace int main() { - CaptureServer srv; - g_srv = &srv; - RestClient client = RestClient::with_base_url(srv.base_url(), SENTINEL, "tok"); - - std::vector> skipped; - json errors = json::array(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. try { - skipped = invoke_all(client); - } catch (const std::exception& e) { - errors.push_back(std::string("invoke_all threw: ") + e.what()); - } + CaptureServer srv; + g_srv = &srv; + RestClient client = RestClient::with_base_url(srv.base_url(), SENTINEL, "tok"); + + std::vector> skipped; + json errors = json::array(); + try { + skipped = invoke_all(client); + } catch (const std::exception& e) { + errors.push_back(std::string("invoke_all threw: ") + e.what()); + } - // Dedupe plan entries by (method, template); keep the first via/call for each. - json route_recs = json::array(); - std::map, PlanEntry> uniq; - std::vector> order; - for (const auto& pe : g_plan) { - auto key = std::make_pair(pe.method, pe.path_template); - if (uniq.find(key) == uniq.end()) { - uniq[key] = pe; - order.push_back(key); + // Dedupe plan entries by (method, template); keep the first via/call for each. + json route_recs = json::array(); + std::map, PlanEntry> uniq; + std::vector> order; + for (const auto& pe : g_plan) { + auto key = std::make_pair(pe.method, pe.path_template); + if (uniq.find(key) == uniq.end()) { + uniq[key] = pe; + order.push_back(key); + } + } + std::sort(order.begin(), order.end()); + for (const auto& key : order) { + const auto& pe = uniq[key]; + route_recs.push_back({{"method", pe.method}, + {"path_template", pe.path_template}, + {"via", pe.via}, + {"call", pe.call}}); } - } - std::sort(order.begin(), order.end()); - for (const auto& key : order) { - const auto& pe = uniq[key]; - route_recs.push_back({{"method", pe.method}, - {"path_template", pe.path_template}, - {"via", pe.via}, - {"call", pe.call}}); - } - json skipped_recs = json::array(); - for (const auto& kr : skipped) { - skipped_recs.push_back({{"key", kr.first}, {"reason", kr.second}}); - } + json skipped_recs = json::array(); + for (const auto& kr : skipped) { + skipped_recs.push_back({{"key", kr.first}, {"reason", kr.second}}); + } - json out = {{"routes", route_recs}, {"skipped", skipped_recs}, {"errors", errors}}; - std::cout << out.dump(2) << "\n"; - return 0; + json out = {{"routes", route_recs}, {"skipped", skipped_recs}, {"errors", errors}}; + std::cout << out.dump(2) << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/secret_scrub_dump.cpp b/tools/secret_scrub_dump.cpp index e3d2dca..1e78c55 100644 --- a/tools/secret_scrub_dump.cpp +++ b/tools/secret_scrub_dump.cpp @@ -251,27 +251,34 @@ std::string drive_and_capture(const std::filesystem::path& capture_path) { } // namespace int main() { - ix::initNetSystem(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + ix::initNetSystem(); - const std::filesystem::path capture = scratch_dir() / "secret_scrub_capture.log"; - const std::string captured = drive_and_capture(capture); + const std::filesystem::path capture = scratch_dir() / "secret_scrub_capture.log"; + const std::string captured = drive_and_capture(capture); - // Forward the captured output to the real stderr so the differ's own - // subprocess-stderr capture sees the identical bytes; stdout stays pure JSON. - if (!captured.empty()) { - std::cerr << captured; - } + // Forward the captured output to the real stderr so the differ's own + // subprocess-stderr capture sees the identical bytes; stdout stays pure JSON. + if (!captured.empty()) { + std::cerr << captured; + } - json out = json::object(); - out["project"] = json::object({{"leaked", captured.find(kProject) != std::string::npos}}); - out["token"] = json::object({{"leaked", captured.find(kToken) != std::string::npos}}); - out["authorization_state"] = - json::object({{"leaked", captured.find(kAuthorizationState) != std::string::npos}}); + json out = json::object(); + out["project"] = json::object({{"leaked", captured.find(kProject) != std::string::npos}}); + out["token"] = json::object({{"leaked", captured.find(kToken) != std::string::npos}}); + out["authorization_state"] = + json::object({{"leaked", captured.find(kAuthorizationState) != std::string::npos}}); - std::cout << out.dump() << std::endl; + std::cout << out.dump() << '\n'; - std::error_code ec; - std::filesystem::remove(capture, ec); - ix::uninitNetSystem(); - return 0; + std::error_code ec; + std::filesystem::remove(capture, ec); + ix::uninitNetSystem(); + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/secure_default_dump.cpp b/tools/secure_default_dump.cpp index 5b32388..362a493 100644 --- a/tools/secure_default_dump.cpp +++ b/tools/secure_default_dump.cpp @@ -6,37 +6,45 @@ // (porting-sdk/scripts/diff_port_secure_default.py, corpus // porting-sdk/scripts/secure_default_corpus.py). // -// The A1 contract: ``define_tool`` defaults ``secure=true`` fleet-wide. A tool -// defined WITHOUT an explicit ``secure`` MUST require SWAIG token validation, -// and the WIRE manifestation of ``secure`` is NOT a ``"secure": true`` key (that -// is not even a property of the SWML UserSWAIGFunction schema) — it is the -// per-tool ``__token`` appended to the rendered function's ``web_hook_url`` when -// the document is rendered with a ``call_id`` (reference agent_base.py:1040 / -// 1096-1100). A tool defined ``secure=false`` gets NO ``__token``. +// PROTOCOL (the 2026-07-27 differ redesign): this program emits the RENDERED +// WIRE PAYLOAD and makes NO judgement about it. Per corpus fixture: // -// For each corpus fixture this program builds a fresh AgentBase, defines the -// tool, renders the SWML with the FIXED corpus call_id (passed as the ``call_id`` -// query parameter — the reference reads it from exactly there, -// swml_service.py:807), and reduces to the deterministic pair the differ -// compares against the python golden: +// {"": {"secure_default_true": bool, "rendered": {}}} // -// secure_default_true — the SDK-recorded ``secure`` flag for the tool. -// wire_reflects_secure — a ``__token`` is present on the rendered webhook IFF -// the tool is secure (secure -> token present; -// insecure -> token absent). +// secure_default_true — the SDK-recorded ``ToolDefinition::secure`` flag, +// read back off the registry (not assumed). +// rendered — that tool's own ``SWAIG.functions[]`` entry, VERBATIM, +// with every nondeterministic token VALUE (an HMAC) +// replaced by the corpus placeholder ````. Every +// KEY and key path is preserved exactly, because the +// KEYS are the whole contract. // -// The token VALUE is an HMAC over (call_id, tool, expiry, nonce) and varies -// run-to-run, so only its PRESENCE folds into the boolean. That keeps the golden -// deterministic while the behavior producing it is real and unfakeable: this -// program cannot report a token for its default tool without the library -// actually minting one onto the wire. +// The differ derives the comparable topology {secure_default_true, +// has_own_webhook, token_carrier} from those keys. The PREVIOUS version of this +// program emitted a self-computed ``wire_reflects_secure`` boolean, which made +// the gate vacuous by construction: the differ never saw the wire, so it could +// not see WHICH key a port had classified on, nor that an INSECURE tool was +// being handed its own (unauthenticated) per-tool ``web_hook_url``. A dump still +// emitting that boolean is now REJECTED as a legacy self-classification. // -// Protocol: stdout = ONE JSON object mapping fixture id -> classification. -// Nothing else is written to stdout (the library logger is suppressed). +// The A1 contract this pins (reference agent_base.py:1040 / 1085-1099): +// define_tool_default_is_secure -> secure_default_true=true, own web_hook_url +// carrying ?__token=. +// define_tool_explicit_insecure -> secure_default_true=false, and NO per-tool +// web_hook_url key at all (it falls back to +// the shared SWAIG.defaults.web_hook_url). +// +// Both tools are registered on ONE agent and rendered in ONE pass, mirroring the +// oracle in diff_port_secure_default.build_oracle. +// +// Protocol: stdout = ONE JSON object. Nothing else is written to stdout (the +// library logger is suppressed). // // Build: the CMake target `secure_default_dump`; the differ invokes // `build/secure_default_dump`. +#include +#include #include #include #include @@ -55,6 +63,7 @@ namespace { const char* kCallId = "call-secure-default-fixture"; const char* kDefaultTool = "sd_default_secure"; const char* kInsecureTool = "sd_explicit_insecure"; +const char* kTokenPlaceholder = ""; // Walk sections.main -> the `ai` verb -> SWAIG.functions and index by name. // Mirrors the oracle's _find_swaig_functions. @@ -88,6 +97,85 @@ std::map swaig_functions_by_name(const json& doc) { return by_name; } +// True iff `s` ends with "token", case-insensitively — the differ's +// _TOKENISH_SUFFIX test for a key that carries a security token. +bool ends_with_token(const std::string& s) { + static const std::string kSuffix = "token"; + if (s.size() < kSuffix.size()) { + return false; + } + const size_t off = s.size() - kSuffix.size(); + for (size_t i = 0; i < kSuffix.size(); ++i) { + const auto c = static_cast(s[off + i]); + if (static_cast(std::tolower(c)) != kSuffix[i]) { + return false; + } + } + return true; +} + +// Replace the VALUE of every token-suffixed query parameter in a URL with the +// placeholder, preserving every key and the parameter order. Mirrors the +// differ's _redact_url_token so its re-application is a fixed point. +std::string redact_url_tokens(const std::string& url) { + const auto q = url.find('?'); + if (q == std::string::npos) { + return url; + } + std::string out = url.substr(0, q + 1); + const std::string query = url.substr(q + 1); + size_t pos = 0; + bool first = true; + while (pos <= query.size()) { + const auto amp = query.find('&', pos); + const std::string pair = + query.substr(pos, amp == std::string::npos ? std::string::npos : amp - pos); + if (!first) { + out += '&'; + } + first = false; + const auto eq = pair.find('='); + if (eq != std::string::npos && ends_with_token(pair.substr(0, eq))) { + out += pair.substr(0, eq + 1); + out += kTokenPlaceholder; + } else { + out += pair; + } + if (amp == std::string::npos) { + break; + } + pos = amp + 1; + } + return out; +} + +// Normalize a rendered functions[] entry: replace every nondeterministic token +// VALUE with the placeholder while preserving every KEY and key path exactly. +// Mirrors the differ's redact_entry. +json redact_entry(const json& entry) { + json out = json::object(); + if (!entry.is_object()) { + return out; + } + for (auto it = entry.begin(); it != entry.end(); ++it) { + const std::string& key = it.key(); + const json& value = it.value(); + if (value.is_string() && ends_with_token(key)) { + out[key] = kTokenPlaceholder; + continue; + } + if (value.is_string()) { + const std::string s = value.get(); + if (s.find("://") != std::string::npos || (!s.empty() && s.front() == '/')) { + out[key] = redact_url_tokens(s); + continue; + } + } + out[key] = value; + } + return out; +} + // FixtureAgent exists solely to READ BACK the SDK-recorded ``secure`` flag off // the protected tool registry rather than assume it. Subclassing is the C++ // idiom for reaching a protected member; it adds NO public library surface (the @@ -105,65 +193,57 @@ class FixtureAgent : public AgentBase { } }; -// True iff a rendered SWAIG function entry's webhook carries the reserved -// ``__token`` query parameter — the wire reflection of ``secure``. Mirrors the -// oracle's _webhook_has_token. -bool webhook_has_token(const std::map& by_name, const std::string& tool) { +// One fixture's dump entry: the SDK-recorded secure flag plus the rendered +// entry with token values redacted. NO classification — the differ does that. +json emit(const std::map& by_name, const std::string& tool, + bool recorded_secure) { auto it = by_name.find(tool); - if (it == by_name.end() || !it->second.contains("web_hook_url") || - !it->second["web_hook_url"].is_string()) { - return false; - } - return it->second["web_hook_url"].get().find("__token=") != std::string::npos; -} - -// Build a fresh agent for one fixture, define the fixture's tool, render with -// the fixed call_id, and reduce to the classification. `explicit_insecure` -// selects the corpus case: false = a DEFAULT define_tool (no explicit secure -// argument, so the library's default applies — this is the case that reds a port -// defaulting secure=false); true = an explicit secure=false define_tool. -json classify(const std::string& tool, bool explicit_insecure) { - FixtureAgent agent; - - const json params = json::object({{"type", "object"}, {"properties", json::object()}}); - signalwire::swaig::ToolHandler handler = [](const json&, const json&) { - return signalwire::swaig::FunctionResult("ok"); - }; - - if (explicit_insecure) { - agent.define_tool(tool, "secure-default fixture tool", params, handler, false); - } else { - // NO explicit secure argument — the library default is what is under test. - agent.define_tool(tool, "secure-default fixture tool", params, handler); - } - - // The SDK-recorded secure flag, read back off the registry (not assumed). - const bool recorded_secure = agent.recorded_secure(tool); - - // Render with the fixed corpus call_id so a secure tool deterministically - // mints its per-tool token. - const std::map query = {{"call_id", kCallId}}; - const json doc = agent.render_swml_for_request(query, json::object(), {}); - const bool token_present = webhook_has_token(swaig_functions_by_name(doc), tool); - + const json entry = it == by_name.end() ? json::object() : it->second; return json::object({ {"secure_default_true", recorded_secure}, - // A token is present IFF the tool is secure. - {"wire_reflects_secure", token_present == recorded_secure}, + {"rendered", redact_entry(entry)}, }); } } // namespace int main() { - // stdout must carry ONLY the JSON classification; the library logger writes - // debug/info to stdout, so suppress it. - signalwire::get_logger().suppress(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + // stdout must carry ONLY the JSON payload; the library logger writes + // debug/info to stdout, so suppress it. + signalwire::get_logger().suppress(); - json out = json::object(); - out["define_tool_default_is_secure"] = classify(kDefaultTool, /*explicit_insecure=*/false); - out["define_tool_explicit_insecure"] = classify(kInsecureTool, /*explicit_insecure=*/true); + FixtureAgent agent; + + const json params = json::object({{"type", "object"}, {"properties", json::object()}}); + signalwire::swaig::ToolHandler handler = [](const json&, const json&) { + return signalwire::swaig::FunctionResult("ok"); + }; - std::cout << out.dump() << std::endl; - return 0; + // Both corpus tools on ONE agent, rendered in ONE pass (mirrors the oracle). + // NO explicit secure argument — the library default is what is under test. + agent.define_tool(kDefaultTool, "secure-default fixture tool", params, handler); + agent.define_tool(kInsecureTool, "secure-default fixture tool", params, handler, false); + + // Render with the fixed corpus call_id so a secure tool deterministically + // mints its per-tool token (the reference reads call_id from exactly this + // query parameter, swml_service.py:807). + const std::map query = {{"call_id", kCallId}}; + const json doc = agent.render_swml_for_request(query, json::object(), {}); + const std::map by_name = swaig_functions_by_name(doc); + + json out = json::object(); + out["define_tool_default_is_secure"] = + emit(by_name, kDefaultTool, agent.recorded_secure(kDefaultTool)); + out["define_tool_explicit_insecure"] = + emit(by_name, kInsecureTool, agent.recorded_secure(kInsecureTool)); + + std::cout << out.dump() << '\n'; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/state_dump.cpp b/tools/state_dump.cpp index a9a0f0a..81ed5ad 100644 --- a/tools/state_dump.cpp +++ b/tools/state_dump.cpp @@ -80,166 +80,193 @@ json submit_answer_delta(const json& args, const json& raw_data) { } // namespace int main() { - // Keep stdout pure JSON — suppress the SDK's INFO logs (which otherwise go to - // stdout) so the differ reads only the JSON object. - signalwire::Logger::instance().suppress(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + // Keep stdout pure JSON — suppress the SDK's INFO logs (which otherwise go to + // stdout) so the differ reads only the JSON object. + signalwire::Logger::instance().suppress(); - json out = json::object(); + json out = json::object(); - // ---- global_data: set MERGES into the accumulated global data ---- - { - AgentBase a = demo_agent(); - a.set_global_data(json{{"company", "SignalWire"}, {"tier", "gold"}}); - out["state_set_global_data"] = a.get_global_data(); - } - { - AgentBase a = demo_agent(); - a.update_global_data(json{{"k1", "v1"}}); - a.update_global_data(json{{"k2", "v2"}}); - out["state_update_global_data"] = a.get_global_data(); - } - { - // MERGE semantics: overlapping key wins, sibling survives. - AgentBase a = demo_agent(); - a.set_global_data(json{{"a", 1}, {"b", 2}}); - a.set_global_data(json{{"b", 99}, {"c", 3}}); - out["state_global_data_merge"] = a.get_global_data(); - } + // ---- global_data: set MERGES into the accumulated global data ---- + { + AgentBase a = demo_agent(); + a.set_global_data(json{{"company", "SignalWire"}, {"tier", "gold"}}); + out["state_set_global_data"] = a.get_global_data(); + } + { + AgentBase a = demo_agent(); + a.update_global_data(json{{"k1", "v1"}}); + a.update_global_data(json{{"k2", "v2"}}); + out["state_update_global_data"] = a.get_global_data(); + } + { + // MERGE semantics: overlapping key wins, sibling survives. + AgentBase a = demo_agent(); + a.set_global_data(json{{"a", 1}, {"b", 2}}); + a.set_global_data(json{{"b", 99}, {"c", 3}}); + out["state_global_data_merge"] = a.get_global_data(); + } - // ---- sip-username registration on AgentBase (lowercased set) ---- - { - AgentBase a = demo_agent(); - a.register_sip_username("Bob"); - a.register_sip_username("alice"); - // The oracle observes sorted(_sip_usernames), so sort the set. - std::vector u = a.get_sip_usernames(); - std::sort(u.begin(), u.end()); - out["state_register_sip_username"] = u; - } - { - // dedup + case-fold: "Bob","BOB","bob" collapse to one. - AgentBase a = demo_agent(); - a.register_sip_username("Bob"); - a.register_sip_username("BOB"); - a.register_sip_username("bob"); - std::vector u = a.get_sip_usernames(); - std::sort(u.begin(), u.end()); - out["state_register_sip_username_dedup"] = u; - } + // ---- sip-username registration on AgentBase (lowercased set) ---- + { + AgentBase a = demo_agent(); + a.register_sip_username("Bob"); + a.register_sip_username("alice"); + // The oracle observes sorted(_sip_usernames), so sort the set. + std::vector u = a.get_sip_usernames(); + std::sort(u.begin(), u.end()); + out["state_register_sip_username"] = u; + } + { + // dedup + case-fold: "Bob","BOB","bob" collapse to one. + AgentBase a = demo_agent(); + a.register_sip_username("Bob"); + a.register_sip_username("BOB"); + a.register_sip_username("bob"); + std::vector u = a.get_sip_usernames(); + std::sort(u.begin(), u.end()); + out["state_register_sip_username_dedup"] = u; + } - // ---- AgentServer sip-username mapping (username -> route) + lookup ---- - { - signalwire::server::AgentServer s; - s.setup_sip_routing("/sip", false); - s.register_sip_username("Bob", "/agent"); - s.register_sip_username("sales", "/sales"); - json lookup_missing = json(nullptr); - std::string missing = s.lookup_sip_route("nope"); - if (!missing.empty()) lookup_missing = missing; - json mapping = json::object(); - for (const auto& [k, v] : s.get_sip_username_mapping()) mapping[k] = v; - out["server_sip_username_mapping"] = json{{"mapping", mapping}, - {"lookup_bob", s.lookup_sip_route("bob")}, - {"lookup_BOB", s.lookup_sip_route("BOB")}, - {"lookup_missing", lookup_missing}}; - } - { - // unregister removes the agent route from the registry. - signalwire::server::AgentServer s; - s.register_(std::make_shared("agent", "/agent"), "/agent"); - s.register_(std::make_shared("other", "/other"), "/other"); - s.unregister("/agent"); - std::vector routes = s.list_routes(); - std::sort(routes.begin(), routes.end()); - out["server_unregister"] = routes; - } + // ---- AgentServer sip-username mapping (username -> route) + lookup ---- + { + signalwire::server::AgentServer s; + s.setup_sip_routing("/sip", false); + s.register_sip_username("Bob", "/agent"); + s.register_sip_username("sales", "/sales"); + json lookup_missing = json(nullptr); + std::string missing = s.lookup_sip_route("nope"); + if (!missing.empty()) { + lookup_missing = missing; + } + json mapping = json::object(); + for (const auto& [k, v] : s.get_sip_username_mapping()) { + mapping[k] = v; + } + out["server_sip_username_mapping"] = json{{"mapping", mapping}, + {"lookup_bob", s.lookup_sip_route("bob")}, + {"lookup_BOB", s.lookup_sip_route("BOB")}, + {"lookup_missing", lookup_missing}}; + } + { + // unregister removes the agent route from the registry. + signalwire::server::AgentServer s; + s.register_(std::make_shared("agent", "/agent"), "/agent"); + s.register_(std::make_shared("other", "/other"), "/other"); + s.unregister("/agent"); + std::vector routes = s.list_routes(); + std::sort(routes.begin(), routes.end()); + out["server_unregister"] = routes; + } - // ---- routing-callback registration on SWMLService (path-normalized) ---- - { - signalwire::swml::Service svc; - svc.set_name("svc"); - svc.set_route("/svc"); - auto noop = [](const json&, const std::map&) -> std::string { - return ""; - }; - svc.register_routing_callback(noop, "/sip/"); - svc.register_routing_callback(noop, "voice"); - out["state_register_routing_callback"] = svc.get_routing_callback_paths(); - } + // ---- routing-callback registration on SWMLService (path-normalized) ---- + { + signalwire::swml::Service svc; + svc.set_name("svc"); + svc.set_route("/svc"); + auto noop = [](const json&, const std::map&) -> std::string { + return ""; + }; + svc.register_routing_callback(noop, "/sip/"); + svc.register_routing_callback(noop, "voice"); + out["state_register_routing_callback"] = svc.get_routing_callback_paths(); + } - // ---- verb-handler registration (VerbHandlerRegistry: ai preloaded) ---- - { - signalwire::core::VerbHandlerRegistry reg; - reg.register_handler(std::make_shared("greet")); - out["state_register_verb_handler"] = json{{"verbs", reg.get_verb_names()}, - {"has_greet", reg.has_handler("greet")}, - {"has_ai", reg.has_handler("ai")}, - {"has_missing", reg.has_handler("nope")}}; - } + // ---- verb-handler registration (VerbHandlerRegistry: ai preloaded) ---- + { + signalwire::core::VerbHandlerRegistry reg; + reg.register_handler(std::make_shared("greet")); + out["state_register_verb_handler"] = json{{"verbs", reg.get_verb_names()}, + {"has_greet", reg.has_handler("greet")}, + {"has_ai", reg.has_handler("ai")}, + {"has_missing", reg.has_handler("nope")}}; + } - // ---- skill registration (SkillRegistry: name -> factory, idempotent) ---- - { - // The C++ SkillRegistry is a global singleton pre-populated with the - // built-in skills, so observe the DELTA: the names this chain adds over the - // pre-existing set (mirrors the oracle's fresh-registry ["custom_alpha", - // "custom_beta"]). Registration is idempotent (a duplicate name is a no-op). - auto& reg = signalwire::skills::SkillRegistry::instance(); - std::set before; - for (const auto& n : reg.list_skills()) before.insert(n); - auto noop_factory = []() -> std::unique_ptr { return nullptr; }; - reg.register_skill("custom_alpha", noop_factory); - reg.register_skill("custom_beta", noop_factory); - reg.register_skill("custom_alpha", noop_factory); // idempotent - std::vector added; - for (const auto& n : reg.list_skills()) { - if (before.find(n) == before.end()) added.push_back(n); + // ---- skill registration (SkillRegistry: name -> factory, idempotent) ---- + { + // The C++ SkillRegistry is a global singleton pre-populated with the + // built-in skills, so observe the DELTA: the names this chain adds over the + // pre-existing set (mirrors the oracle's fresh-registry ["custom_alpha", + // "custom_beta"]). + // + // NOTE: this used to re-register "custom_alpha" a second time to assert that + // registration was idempotent. It is NOT idempotent any more: f0b5df5 made + // register_skill THROW on a duplicate name, because silent overwriting is what + // let two different classes both claim "spider" and hid which one was live. + // The duplicate call therefore aborted this tool (exit 134), the differ saw + // empty stdout, and BEHAVIORAL-STATE went red. The delta computed below already + // proves the registry does not grow spuriously, so the duplicate call bought + // nothing the rest of this block does not. + auto& reg = signalwire::skills::SkillRegistry::instance(); + std::set before; + for (const auto& n : reg.list_skills()) { + before.insert(n); + } + auto noop_factory = []() -> std::unique_ptr { + return nullptr; + }; + reg.register_skill("custom_alpha", noop_factory); + reg.register_skill("custom_beta", noop_factory); + std::vector added; + for (const auto& n : reg.list_skills()) { + if (before.find(n) == before.end()) { + added.push_back(n); + } + } + std::sort(added.begin(), added.end()); + out["state_register_skill"] = added; } - std::sort(added.begin(), added.end()); - out["state_register_skill"] = added; - } - // ---- InfoGatherer.submit_answer: records answer + advances index ---- - out["infogatherer_submit_answer_first"] = submit_answer_delta( - json{{"answer", "Alice"}}, - json{{"global_data", - {{"questions", - json::array({json{{"key_name", "name"}, {"question_text", "What is your name?"}}, - json{{"key_name", "email"}, {"question_text", "What is your email?"}}})}, - {"question_index", 0}, - {"answers", json::array()}}}}); - out["infogatherer_submit_answer_last"] = submit_answer_delta( - json{{"answer", "a@b.com"}}, - json{{"global_data", - {{"questions", - json::array({json{{"key_name", "name"}, {"question_text", "What is your name?"}}, - json{{"key_name", "email"}, {"question_text", "What is your email?"}}})}, - {"question_index", 1}, - {"answers", json::array({json{{"key_name", "name"}, {"answer", "Alice"}}})}}}}); - - // ---- contexts/steps navigation (valid_steps rendered per step) ---- - { - AgentBase a = demo_agent(); - auto& cb = a.define_contexts(); - auto& ctx = cb.add_context("default"); - ctx.add_step("greet", "Greet the caller.", {}, "", std::nullopt, {"collect"}); - ctx.add_step("collect", "Collect their info.", {}, "", std::nullopt, {"greet"}); - json rendered = cb.to_json(); - json nav = json::object(); - for (auto it = rendered.begin(); it != rendered.end(); ++it) { - json steps = json::array(); - if (it.value().contains("steps") && it.value()["steps"].is_array()) { - for (const auto& s : it.value()["steps"]) { - json reduced = json::object(); - reduced["name"] = s.contains("name") ? s["name"] : json(nullptr); - reduced["valid_steps"] = s.contains("valid_steps") ? s["valid_steps"] : json(nullptr); - steps.push_back(reduced); + // ---- InfoGatherer.submit_answer: records answer + advances index ---- + out["infogatherer_submit_answer_first"] = submit_answer_delta( + json{{"answer", "Alice"}}, + json{{"global_data", + {{"questions", + json::array( + {json{{"key_name", "name"}, {"question_text", "What is your name?"}}, + json{{"key_name", "email"}, {"question_text", "What is your email?"}}})}, + {"question_index", 0}, + {"answers", json::array()}}}}); + out["infogatherer_submit_answer_last"] = submit_answer_delta( + json{{"answer", "a@b.com"}}, + json{{"global_data", + {{"questions", + json::array( + {json{{"key_name", "name"}, {"question_text", "What is your name?"}}, + json{{"key_name", "email"}, {"question_text", "What is your email?"}}})}, + {"question_index", 1}, + {"answers", json::array({json{{"key_name", "name"}, {"answer", "Alice"}}})}}}}); + + // ---- contexts/steps navigation (valid_steps rendered per step) ---- + { + AgentBase a = demo_agent(); + auto& cb = a.define_contexts(); + auto& ctx = cb.add_context("default"); + ctx.add_step("greet", "Greet the caller.", {}, "", std::nullopt, {"collect"}); + ctx.add_step("collect", "Collect their info.", {}, "", std::nullopt, {"greet"}); + json rendered = cb.to_json(); + json nav = json::object(); + for (auto it = rendered.begin(); it != rendered.end(); ++it) { + json steps = json::array(); + if (it.value().contains("steps") && it.value()["steps"].is_array()) { + for (const auto& s : it.value()["steps"]) { + json reduced = json::object(); + reduced["name"] = s.contains("name") ? s["name"] : json(nullptr); + reduced["valid_steps"] = s.contains("valid_steps") ? s["valid_steps"] : json(nullptr); + steps.push_back(reduced); + } } + nav[it.key()] = steps; } - nav[it.key()] = steps; + out["state_contexts_navigation"] = nav; } - out["state_contexts_navigation"] = nav; - } - std::cout << out.dump() << "\n"; - return 0; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/strict_render_dump.cpp b/tools/strict_render_dump.cpp index 4f6556d..fb5da22 100644 --- a/tools/strict_render_dump.cpp +++ b/tools/strict_render_dump.cpp @@ -58,7 +58,12 @@ std::string outcome(const std::function& build) { // add_verb strict-checks. Service is non-copyable, hence built in place in the // closure. std::function add_verb(const std::string& name, const json& config) { - return [name, config]() { + // This lambda THROWING is the mechanism, not a defect: outcome() above calls + // it inside a try/catch and classifies a throw as "raised", which is the + // corpus value this tool exists to record. Swallowing the exception here + // would make every strict-validation case report "ok" and silently gut the + // BEHAVIORAL-STRICT-RENDER comparison against the Python differ. + return [name, config]() { // NOLINT(bugprone-exception-escape) Service svc; svc.set_name("s").set_route("/s"); svc.add_verb(name, config); @@ -74,84 +79,91 @@ signalwire::swaig::FunctionResult noop_handler(const json&, const json&) { } // namespace int main() { - json out = json::object(); - - // ================================================================ - // Verb-level strict render (SWMLService, validation ON) - // ================================================================ - out["strict_unknown_verb"] = outcome(add_verb("foobar", json::object())); - out["strict_answer_misspelled_key"] = outcome(add_verb("answer", json{{"maxduration", 5}})); - out["strict_answer_unknown_key"] = outcome(add_verb("answer", json{{"wibble", 1}})); - out["strict_play_misspelled_key"] = - outcome(add_verb("play", json{{"urlz", json::array({"say:hi"})}})); - out["strict_play_valid_plus_unknown_key"] = - outcome(add_verb("play", json{{"url", "say:hi"}, {"foo", 1}})); - out["strict_record_misspelled_key"] = outcome(add_verb("record", json{{"formatt", "wav"}})); - out["strict_answer_wrong_type"] = - outcome(add_verb("answer", json{{"max_duration", "notanumber"}})); - out["strict_ai_misspelled_top_key"] = - outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"temperatur", 0.5}})); - out["strict_ai_unknown_top_key"] = - outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"zzz", 1}})); - out["strict_ai_missing_prompt"] = - outcome(add_verb("ai", json{{"post_prompt", {{"text", "bye"}}}})); - - // good documents must still render - out["strict_answer_ok"] = outcome(add_verb("answer", json{{"max_duration", 5}})); - out["strict_play_ok"] = outcome(add_verb("play", json{{"url", "say:hi"}})); - out["strict_ai_ok"] = outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}})); - out["strict_ai_params_open_ok"] = outcome( - add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"params", {{"some_future_param", 1}}}})); - - // ================================================================ - // Contexts-level strict render (AgentBase; dangling refs) - // ================================================================ - - // strict_dangling_step_function: order_status registered, step whitelists an - // unregistered non-native 'get_datetime' -> dangling -> raise. - out["strict_dangling_step_function"] = outcome([]() { - AgentBase a("a", "/a"); - json params = json{{"type", "object"}, {"properties", json::object()}}; - a.define_tool("order_status", "look up an order", params, noop_handler); - auto& cb = a.define_contexts(); - auto& st = cb.add_context("default").add_step("help"); - st.set_text("help"); - st.set_functions(std::vector{"order_status", "get_datetime"}); - cb.validate(); - }); - - // strict_registered_step_function_ok: step whitelists a registered tool. - out["strict_registered_step_function_ok"] = outcome([]() { - AgentBase a("a", "/a"); - json params = json{{"type", "object"}, {"properties", json::object()}}; - a.define_tool("order_status", "look up an order", params, noop_handler); - auto& cb = a.define_contexts(); - auto& st = cb.add_context("default").add_step("help"); - st.set_text("help"); - st.set_functions(std::vector{"order_status"}); - cb.validate(); - }); - - // strict_reserved_native_function_ok: reserved natives are not dangling. - out["strict_reserved_native_function_ok"] = outcome([]() { - AgentBase a("a", "/a"); - auto& cb = a.define_contexts(); - auto& st = cb.add_context("default").add_step("help"); - st.set_text("help"); - st.set_functions(std::vector{"next_step", "change_context"}); - cb.validate(); - }); - - // strict_dangling_valid_context: valid_contexts references an undefined context. - out["strict_dangling_valid_context"] = outcome([]() { - AgentBase a("a", "/a"); - auto& cb = a.define_contexts(); - auto& st = cb.add_context("default").add_step("help"); - st.set_text("help"); - st.set_valid_contexts(std::vector{"nowhere"}); - cb.validate(); - }); - - std::cout << out.dump() << "\n"; - return 0; + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); + + // ================================================================ + // Verb-level strict render (SWMLService, validation ON) + // ================================================================ + out["strict_unknown_verb"] = outcome(add_verb("foobar", json::object())); + out["strict_answer_misspelled_key"] = outcome(add_verb("answer", json{{"maxduration", 5}})); + out["strict_answer_unknown_key"] = outcome(add_verb("answer", json{{"wibble", 1}})); + out["strict_play_misspelled_key"] = + outcome(add_verb("play", json{{"urlz", json::array({"say:hi"})}})); + out["strict_play_valid_plus_unknown_key"] = + outcome(add_verb("play", json{{"url", "say:hi"}, {"foo", 1}})); + out["strict_record_misspelled_key"] = outcome(add_verb("record", json{{"formatt", "wav"}})); + out["strict_answer_wrong_type"] = + outcome(add_verb("answer", json{{"max_duration", "notanumber"}})); + out["strict_ai_misspelled_top_key"] = + outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"temperatur", 0.5}})); + out["strict_ai_unknown_top_key"] = + outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"zzz", 1}})); + out["strict_ai_missing_prompt"] = + outcome(add_verb("ai", json{{"post_prompt", {{"text", "bye"}}}})); + + // good documents must still render + out["strict_answer_ok"] = outcome(add_verb("answer", json{{"max_duration", 5}})); + out["strict_play_ok"] = outcome(add_verb("play", json{{"url", "say:hi"}})); + out["strict_ai_ok"] = outcome(add_verb("ai", json{{"prompt", {{"text", "hi"}}}})); + out["strict_ai_params_open_ok"] = outcome( + add_verb("ai", json{{"prompt", {{"text", "hi"}}}, {"params", {{"some_future_param", 1}}}})); + + // ================================================================ + // Contexts-level strict render (AgentBase; dangling refs) + // ================================================================ + + // strict_dangling_step_function: order_status registered, step whitelists an + // unregistered non-native 'get_datetime' -> dangling -> raise. + out["strict_dangling_step_function"] = outcome([]() { + AgentBase a("a", "/a"); + json params = json{{"type", "object"}, {"properties", json::object()}}; + a.define_tool("order_status", "look up an order", params, noop_handler); + auto& cb = a.define_contexts(); + auto& st = cb.add_context("default").add_step("help"); + st.set_text("help"); + st.set_functions(std::vector{"order_status", "get_datetime"}); + cb.validate(); + }); + + // strict_registered_step_function_ok: step whitelists a registered tool. + out["strict_registered_step_function_ok"] = outcome([]() { + AgentBase a("a", "/a"); + json params = json{{"type", "object"}, {"properties", json::object()}}; + a.define_tool("order_status", "look up an order", params, noop_handler); + auto& cb = a.define_contexts(); + auto& st = cb.add_context("default").add_step("help"); + st.set_text("help"); + st.set_functions(std::vector{"order_status"}); + cb.validate(); + }); + + // strict_reserved_native_function_ok: reserved natives are not dangling. + out["strict_reserved_native_function_ok"] = outcome([]() { + AgentBase a("a", "/a"); + auto& cb = a.define_contexts(); + auto& st = cb.add_context("default").add_step("help"); + st.set_text("help"); + st.set_functions(std::vector{"next_step", "change_context"}); + cb.validate(); + }); + + // strict_dangling_valid_context: valid_contexts references an undefined context. + out["strict_dangling_valid_context"] = outcome([]() { + AgentBase a("a", "/a"); + auto& cb = a.define_contexts(); + auto& st = cb.add_context("default").add_step("help"); + st.set_text("help"); + st.set_valid_contexts(std::vector{"nowhere"}); + cb.validate(); + }); + + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/swml_dump.cpp b/tools/swml_dump.cpp index 59ab37f..9ac52eb 100644 --- a/tools/swml_dump.cpp +++ b/tools/swml_dump.cpp @@ -79,10 +79,14 @@ json extract(const json& doc, const std::string& path) { // pick reduces a map fragment to the listed keys (mirrors the oracle's `pick`). json pick(const json& frag, const std::vector& keys) { - if (!frag.is_object()) return frag; + if (!frag.is_object()) { + return frag; + } json out = json::object(); for (const auto& k : keys) { - if (frag.contains(k)) out[k] = frag[k]; + if (frag.contains(k)) { + out[k] = frag[k]; + } } return out; } @@ -92,92 +96,100 @@ json render(const AgentBase& a) { return a.render_swml(); } } // namespace int main() { - json out = json::object(); - - // swml_set_prompt_llm_params: two set_prompt_llm_params calls MERGE. - { - AgentBase a = new_agent(); - a.set_prompt_llm_params(json{{"temperature", 0.5}}); - a.set_prompt_llm_params(json{{"top_p", 0.9}}); - out["swml_set_prompt_llm_params"] = - pick(extract(render(a), "ai.prompt"), {"temperature", "top_p"}); - } + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); + + // swml_set_prompt_llm_params: two set_prompt_llm_params calls MERGE. + { + AgentBase a = new_agent(); + a.set_prompt_llm_params(json{{"temperature", 0.5}}); + a.set_prompt_llm_params(json{{"top_p", 0.9}}); + out["swml_set_prompt_llm_params"] = + pick(extract(render(a), "ai.prompt"), {"temperature", "top_p"}); + } - // swml_set_post_prompt_llm_params: establish a post-prompt, then merge params. - { - AgentBase a = new_agent(); - a.set_post_prompt("Summarize the call."); - a.set_post_prompt_llm_params(json{{"temperature", 0.3}}); - a.set_post_prompt_llm_params(json{{"top_p", 0.8}}); - out["swml_set_post_prompt_llm_params"] = - pick(extract(render(a), "ai.post_prompt"), {"temperature", "top_p"}); - } + // swml_set_post_prompt_llm_params: establish a post-prompt, then merge params. + { + AgentBase a = new_agent(); + a.set_post_prompt("Summarize the call."); + a.set_post_prompt_llm_params(json{{"temperature", 0.3}}); + a.set_post_prompt_llm_params(json{{"top_p", 0.8}}); + out["swml_set_post_prompt_llm_params"] = + pick(extract(render(a), "ai.post_prompt"), {"temperature", "top_p"}); + } - // swml_add_language: engine/model/voice carried into ai.languages. - { - AgentBase a = new_agent(); - LanguageConfig lang; - lang.name = "English"; - lang.code = "en-US"; - lang.voice = "rime.spore"; - lang.engine = "rime"; - lang.model = "mistv2"; - a.add_language(lang); - out["swml_add_language"] = extract(render(a), "ai.languages"); - } + // swml_add_language: engine/model/voice carried into ai.languages. + { + AgentBase a = new_agent(); + LanguageConfig lang; + lang.name = "English"; + lang.code = "en-US"; + lang.voice = "rime.spore"; + lang.engine = "rime"; + lang.model = "mistv2"; + a.add_language(lang); + out["swml_add_language"] = extract(render(a), "ai.languages"); + } - // swml_add_pattern_hint: structured hint into ai.hints. - { - AgentBase a = new_agent(); - a.add_pattern_hint("SignalWire", "signal wire", "SignalWire", true); - out["swml_add_pattern_hint"] = extract(render(a), "ai.hints"); - } + // swml_add_pattern_hint: structured hint into ai.hints. + { + AgentBase a = new_agent(); + a.add_pattern_hint("SignalWire", "signal wire", "SignalWire", true); + out["swml_add_pattern_hint"] = extract(render(a), "ai.hints"); + } - // swml_add_hint: a plain string hint. - { - AgentBase a = new_agent(); - a.add_hint("SignalWire"); - out["swml_add_hint"] = extract(render(a), "ai.hints"); - } + // swml_add_hint: a plain string hint. + { + AgentBase a = new_agent(); + a.add_hint("SignalWire"); + out["swml_add_hint"] = extract(render(a), "ai.hints"); + } - // swml_prompt_add_section: POM sections render into ai.prompt.pom. - { - AgentBase a = new_agent(); - a.prompt_add_section("Role", "You are a helpful assistant.", {}); - a.prompt_add_section("Rules", "", {"Be concise", "Be accurate"}); - out["swml_prompt_add_section"] = extract(render(a), "ai.prompt.pom"); - } + // swml_prompt_add_section: POM sections render into ai.prompt.pom. + { + AgentBase a = new_agent(); + a.prompt_add_section("Role", "You are a helpful assistant.", {}); + a.prompt_add_section("Rules", "", {"Be concise", "Be accurate"}); + out["swml_prompt_add_section"] = extract(render(a), "ai.prompt.pom"); + } - // swml_add_pronunciation: renders into ai.pronounce. - { - AgentBase a = new_agent(); - a.add_pronunciation("SW", "SignalWire", true); - out["swml_add_pronunciation"] = extract(render(a), "ai.pronounce"); - } + // swml_add_pronunciation: renders into ai.pronounce. + { + AgentBase a = new_agent(); + a.add_pronunciation("SW", "SignalWire", true); + out["swml_add_pronunciation"] = extract(render(a), "ai.pronounce"); + } - // swml_define_tool_complete_schema: define_tool with a COMPLETE - // {type,properties,required} schema must render ai.SWAIG.functions[?lookup] - // .parameters as that schema FLAT (pass-through), NOT double-wrapped. - { - AgentBase a = new_agent(); - json schema = json{{"type", "object"}, - {"properties", {{"q", {{"type", "string"}}}}}, - {"required", json::array({"q"})}}; - a.define_tool("lookup", "Look up a thing", schema, - [](const json&, const json&) { return signalwire::swaig::FunctionResult("ok"); }); - json funcs = extract(render(a), "ai.SWAIG.functions"); - json params = json(nullptr); - if (funcs.is_array()) { - for (const auto& f : funcs) { - if (f.is_object() && f.value("function", "") == "lookup" && f.contains("parameters")) { - params = f["parameters"]; - break; + // swml_define_tool_complete_schema: define_tool with a COMPLETE + // {type,properties,required} schema must render ai.SWAIG.functions[?lookup] + // .parameters as that schema FLAT (pass-through), NOT double-wrapped. + { + AgentBase a = new_agent(); + json schema = json{{"type", "object"}, + {"properties", {{"q", {{"type", "string"}}}}}, + {"required", json::array({"q"})}}; + a.define_tool("lookup", "Look up a thing", schema, [](const json&, const json&) { + return signalwire::swaig::FunctionResult("ok"); + }); + json funcs = extract(render(a), "ai.SWAIG.functions"); + json params = json(nullptr); + if (funcs.is_array()) { + for (const auto& f : funcs) { + if (f.is_object() && f.value("function", "") == "lookup" && f.contains("parameters")) { + params = f["parameters"]; + break; + } } } + out["swml_define_tool_complete_schema"] = params; } - out["swml_define_tool_complete_schema"] = params; - } - std::cout << out.dump() << "\n"; - return 0; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/token_interop_mint.cpp b/tools/token_interop_mint.cpp new file mode 100644 index 0000000..6c0c3bc --- /dev/null +++ b/tools/token_interop_mint.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2025 SignalWire +// SPDX-License-Identifier: MIT +// +// token_interop_mint.cpp — the C++ port's TOKEN-INTEROP mint fixture for the +// cross-port checker (porting-sdk/scripts/diff_port_token_interop.py). +// +// The contract being proven is property 3 of the SWAIG tool-token contract: a +// token this port MINTS must validate under the REFERENCE's own decoder. The +// other two properties (that a token is minted at all; that the HMAC is keyed +// with the ``secret_key`` STRING's bytes) already had coverage — this one did +// not, and a port can pass both and still emit a token no other implementation +// accepts, in which case every secure tool call fails authentication in +// production. +// +// PROTOCOL: read the FIXED mint inputs from the environment (the checker owns +// them, so this fixture cannot drift from the values it is verified against), +// construct a ``SessionManager`` with that secret key, mint ONE token, and print +// JUST the token on stdout. Anything else goes to stderr. +// +// Run from the signalwire-cpp repo root, after building: +// +// build/token_interop_mint + +#include +#include +#include + +#include "signalwire/security/session_manager.hpp" + +namespace { + +// Read a required fixed mint input from the environment, or fail loud. +std::string required(const char* name) { + const char* value = std::getenv(name); + if (value == nullptr || *value == '\0') { + std::cerr << name + << " is not set — the TOKEN-INTEROP checker supplies the fixed mint " + "inputs in the environment; run this via " + "diff_port_token_interop.py --mint-cmd.\n"; + std::exit(1); + } + return std::string(value); +} + +} // namespace + +int main() { + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + const std::string secret_key = required("SW_TOKEN_INTEROP_SECRET_KEY"); + const std::string call_id = required("SW_TOKEN_INTEROP_CALL_ID"); + const std::string function_name = required("SW_TOKEN_INTEROP_FUNCTION_NAME"); + + // Default expiry — the token must carry a FUTURE expiry, which the checker + // verifies. The (int, const std::string&) constructor takes the reference's + // ``secret_key`` STRING, whose bytes key the HMAC (NOT 32 raw bytes). + const signalwire::security::SessionManager manager(900, secret_key); + std::cout << manager.generate_token(function_name, call_id) << '\n'; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } +} diff --git a/tools/wait_liveness_dump.cpp b/tools/wait_liveness_dump.cpp index 5cbb748..421f6b1 100644 --- a/tools/wait_liveness_dump.cpp +++ b/tools/wait_liveness_dump.cpp @@ -117,76 +117,83 @@ Call* answered_call(RelayClient& client) { } // namespace int main() { - // Silence the SDK logger programmatically so ONLY the JSON reaches stdout (the - // logger writes INFO to std::cout; an env-only guard is racy against the RELAY - // connect log). Mirrors wire_relay_dump. - signalwire::Logger::instance().suppress(); - json out = json::object(); - - // ---- live_play_wait ----------------------------------------------------- - { - auto client = mt::make_client(); - Call* call = answered_call(*client); - arm_finished("calling.play"); - Action action = call->play(play_media(), 0.0, kCid); - out["live_play_wait"] = classify(drive(action)); - client->disconnect(); - } - - // ---- live_record_wait --------------------------------------------------- - { - auto client = mt::make_client(); - Call* call = answered_call(*client); - arm_finished("calling.record"); - Action action = call->record({{"audio", {{"format", "mp3"}}}}, kCid); - out["live_record_wait"] = classify(drive(action)); - client->disconnect(); - } + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + // Silence the SDK logger programmatically so ONLY the JSON reaches stdout (the + // logger writes INFO to std::cout; an env-only guard is racy against the RELAY + // connect log). Mirrors wire_relay_dump. + signalwire::Logger::instance().suppress(); + json out = json::object(); + + // ---- live_play_wait ----------------------------------------------------- + { + auto client = mt::make_client(); + Call* call = answered_call(*client); + arm_finished("calling.play"); + Action action = call->play(play_media(), 0.0, kCid); + out["live_play_wait"] = classify(drive(action)); + client->disconnect(); + } - // ---- live_nested_wait --------------------------------------------------- - // The "wait inside on_completed" re-entrancy pattern. Like the python oracle - // (diff_port_wait_liveness.py::_drive_nested) and the rust dump, the inner wait - // is driven right AFTER the outer wait returns (not synchronously inside the - // completion callback, which fires on the read-loop thread and would deadlock a - // thread-blocking wait). It still exercises re-entrancy of the receive path: the - // inner wait pumps the same connection the outer just used. FOLD: timed_out if - // EITHER hung, blocked only if BOTH blocked, completed_state from the inner. - { - auto client = mt::make_client(); - Call* call = answered_call(*client); - - arm_finished("calling.play"); - Action outer = call->play(play_media(), 0.0, kCid); - json outer_cls = classify(drive(outer)); - - json folded; - if (outer_cls.value("timed_out", false)) { - folded = {{"blocked_until_event", false}, - {"returned_after_event", false}, - {"completed_state", ""}, - {"timed_out", true}}; - } else { + // ---- live_record_wait --------------------------------------------------- + { + auto client = mt::make_client(); + Call* call = answered_call(*client); arm_finished("calling.record"); - Action inner = call->record({{"audio", {{"format", "mp3"}}}}, kCid); - json inner_cls = classify(drive(inner)); - if (inner_cls.value("timed_out", false)) { + Action action = call->record({{"audio", {{"format", "mp3"}}}}, kCid); + out["live_record_wait"] = classify(drive(action)); + client->disconnect(); + } + + // ---- live_nested_wait --------------------------------------------------- + // The "wait inside on_completed" re-entrancy pattern. Like the python oracle + // (diff_port_wait_liveness.py::_drive_nested) and the rust dump, the inner wait + // is driven right AFTER the outer wait returns (not synchronously inside the + // completion callback, which fires on the read-loop thread and would deadlock a + // thread-blocking wait). It still exercises re-entrancy of the receive path: the + // inner wait pumps the same connection the outer just used. FOLD: timed_out if + // EITHER hung, blocked only if BOTH blocked, completed_state from the inner. + { + auto client = mt::make_client(); + Call* call = answered_call(*client); + + arm_finished("calling.play"); + Action outer = call->play(play_media(), 0.0, kCid); + json outer_cls = classify(drive(outer)); + + json folded; + if (outer_cls.value("timed_out", false)) { folded = {{"blocked_until_event", false}, {"returned_after_event", false}, {"completed_state", ""}, {"timed_out", true}}; } else { - bool both_blocked = outer_cls.value("blocked_until_event", false) && - inner_cls.value("blocked_until_event", false); - folded = {{"blocked_until_event", both_blocked}, - {"returned_after_event", true}, - {"completed_state", inner_cls.value("completed_state", "")}, - {"timed_out", false}}; + arm_finished("calling.record"); + Action inner = call->record({{"audio", {{"format", "mp3"}}}}, kCid); + json inner_cls = classify(drive(inner)); + if (inner_cls.value("timed_out", false)) { + folded = {{"blocked_until_event", false}, + {"returned_after_event", false}, + {"completed_state", ""}, + {"timed_out", true}}; + } else { + bool both_blocked = outer_cls.value("blocked_until_event", false) && + inner_cls.value("blocked_until_event", false); + folded = {{"blocked_until_event", both_blocked}, + {"returned_after_event", true}, + {"completed_state", inner_cls.value("completed_state", "")}, + {"timed_out", false}}; + } } + out["live_nested_wait"] = folded; + client->disconnect(); } - out["live_nested_wait"] = folded; - client->disconnect(); - } - std::cout << out.dump() << "\n"; - return 0; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/wire_dump.cpp b/tools/wire_dump.cpp index 3687e7b..7eb7ef1 100644 --- a/tools/wire_dump.cpp +++ b/tools/wire_dump.cpp @@ -56,7 +56,7 @@ std::string hmac_hex(const EVP_MD* evp, const std::string& key, const std::strin reinterpret_cast(data.data()), data.size(), md.data(), &md_len); static const char* kHex = "0123456789abcdef"; std::string out; - out.reserve(md_len * 2); + out.reserve(static_cast(md_len) * 2); for (unsigned int i = 0; i < md_len; ++i) { out.push_back(kHex[md[i] >> 4]); out.push_back(kHex[md[i] & 0x0F]); @@ -115,163 +115,202 @@ std::string oracle_sig(const std::string& url, const std::string& body, const st } // namespace int main() { - json out = json::object(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + json out = json::object(); - // Key the manager with the secret STRING, matching the reference's - // ``SessionManager(secret_key=SECRET)`` — the HMAC key is the bytes of that - // string. The byte-vector constructor is NOT equivalent here: it hex-encodes - // its input into ``secret_key_``, so passing kSecret's bytes would sign with - // hex(kSecret) while ``oracle_token`` below signs with kSecret itself, and - // the cross-port token_interop check would compare two different keys. - SessionManager sm(900, kSecret); + // Key the manager with the secret STRING, matching the reference's + // ``SessionManager(secret_key=SECRET)`` — the HMAC key is the bytes of that + // string. The byte-vector constructor is NOT equivalent here: it hex-encodes + // its input into ``secret_key_``, so passing kSecret's bytes would sign with + // hex(kSecret) while ``oracle_token`` below signs with kSecret itself, and + // the cross-port token_interop check would compare two different keys. + SessionManager sm(900, kSecret); - // token_format: generate a token via the SDK, decode its wire-format fields. - { - std::string token = sm.generate_token("my_func", "call_1"); - // Decode base64url -> raw dotted string. The SDK produces a padded - // base64url token; nlohmann/std don't decode base64url, so decode inline. - // Simpler: re-derive fields by regenerating is impossible (random nonce), - // so decode the token the SDK emitted. - auto b64url_decode = [](const std::string& s) { - auto val = [](char c) -> int { - if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a' + 26; - if (c >= '0' && c <= '9') return c - '0' + 52; - if (c == '-') return 62; - if (c == '_') return 63; - return -1; + // token_format: generate a token via the SDK, decode its wire-format fields. + { + std::string token = sm.generate_token("my_func", "call_1"); + // Decode base64url -> raw dotted string. The SDK produces a padded + // base64url token; nlohmann/std don't decode base64url, so decode inline. + // Simpler: re-derive fields by regenerating is impossible (random nonce), + // so decode the token the SDK emitted. + auto b64url_decode = [](const std::string& s) { + auto val = [](char c) -> int { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '-') { + return 62; + } + if (c == '_') { + return 63; + } + return -1; + }; + std::string decoded; + uint32_t buf = 0; + int bits = 0; + for (char c : s) { + if (c == '=') { + break; + } + int v = val(c); + if (v < 0) { + continue; + } + buf = (buf << 6) | static_cast(v); + bits += 6; + if (bits >= 8) { + bits -= 8; + decoded.push_back(static_cast((buf >> bits) & 0xFF)); + } + } + return decoded; }; - std::string decoded; - uint32_t buf = 0; - int bits = 0; - for (char c : s) { - if (c == '=') break; - int v = val(c); - if (v < 0) continue; - buf = (buf << 6) | static_cast(v); - bits += 6; - if (bits >= 8) { - bits -= 8; - decoded.push_back(static_cast((buf >> bits) & 0xFF)); + std::string raw = b64url_decode(token); + std::vector parts; + { + std::string cur; + for (char c : raw) { + if (c == '.') { + parts.push_back(cur); + cur.clear(); + } else { + cur.push_back(c); + } } + parts.push_back(cur); } - return decoded; - }; - std::string raw = b64url_decode(token); - std::vector parts; - { - std::string cur; - for (char c : raw) { - if (c == '.') { - parts.push_back(cur); - cur.clear(); - } else { - cur.push_back(c); + std::string nonce = parts.size() > 3 ? parts[3] : ""; + bool nonce_is_hex = parts.size() > 3; + for (char c : nonce) { + if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { + nonce_is_hex = false; + break; } } - parts.push_back(cur); + json tf = json::object(); + tf["n_fields"] = parts.size(); + tf["call_id"] = !parts.empty() ? json(parts[0]) : json(nullptr); + tf["function_name"] = parts.size() > 1 ? json(parts[1]) : json(nullptr); + tf["nonce_len"] = nonce.size(); + tf["nonce_is_hex"] = nonce_is_hex; + out["token_format"] = tf; } - std::string nonce = parts.size() > 3 ? parts[3] : ""; - bool nonce_is_hex = parts.size() > 3; - for (char c : nonce) { - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { - nonce_is_hex = false; - break; - } - } - json tf = json::object(); - tf["n_fields"] = parts.size(); - tf["call_id"] = parts.size() > 0 ? json(parts[0]) : json(nullptr); - tf["function_name"] = parts.size() > 1 ? json(parts[1]) : json(nullptr); - tf["nonce_len"] = nonce.size(); - tf["nonce_is_hex"] = nonce_is_hex; - out["token_format"] = tf; - } - // token_nonce_distinct: two generations must differ (random nonce). - { - std::string n1 = sm.generate_token("f", "c"); - std::string n2 = sm.generate_token("f", "c"); - out["token_nonce_distinct"] = json{{"distinct", n1 != n2}}; - } + // token_nonce_distinct: two generations must differ (random nonce). + { + std::string n1 = sm.generate_token("f", "c"); + std::string n2 = sm.generate_token("f", "c"); + out["token_nonce_distinct"] = json{{"distinct", n1 != n2}}; + } - // token_interop: validate an oracle-format token built from SECRET. - { - std::string tok = oracle_token("oracle_call", "oracle_fn"); - out["token_interop"] = json{{"valid", sm.validate_token(tok, "oracle_fn", "oracle_call")}}; - } + // token_interop: validate an oracle-format token built from SECRET. + { + std::string tok = oracle_token("oracle_call", "oracle_fn"); + out["token_interop"] = json{{"valid", sm.validate_token(tok, "oracle_fn", "oracle_call")}}; + } - // token_tamper_rejected: a one-byte-flipped signature must fail. Build the - // oracle token, decode, flip the first byte of the signature (after the last - // '.'), re-encode — the mirror of Go's tamperedToken / _tampered_token. - { - std::string tok = oracle_token("c", "f"); - // decode - auto b64url_decode = [](const std::string& s) { - auto val = [](char c) -> int { - if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a' + 26; - if (c >= '0' && c <= '9') return c - '0' + 52; - if (c == '-') return 62; - if (c == '_') return 63; - return -1; - }; - std::string decoded; - uint32_t buf = 0; - int bits = 0; - for (char c : s) { - if (c == '=') break; - int v = val(c); - if (v < 0) continue; - buf = (buf << 6) | static_cast(v); - bits += 6; - if (bits >= 8) { - bits -= 8; - decoded.push_back(static_cast((buf >> bits) & 0xFF)); + // token_tamper_rejected: a one-byte-flipped signature must fail. Build the + // oracle token, decode, flip the first byte of the signature (after the last + // '.'), re-encode — the mirror of Go's tamperedToken / _tampered_token. + { + std::string tok = oracle_token("c", "f"); + // decode + auto b64url_decode = [](const std::string& s) { + auto val = [](char c) -> int { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '-') { + return 62; + } + if (c == '_') { + return 63; + } + return -1; + }; + std::string decoded; + uint32_t buf = 0; + int bits = 0; + for (char c : s) { + if (c == '=') { + break; + } + int v = val(c); + if (v < 0) { + continue; + } + buf = (buf << 6) | static_cast(v); + bits += 6; + if (bits >= 8) { + bits -= 8; + decoded.push_back(static_cast((buf >> bits) & 0xFF)); + } } + return decoded; + }; + std::string raw = b64url_decode(tok); + size_t last = raw.rfind('.'); + if (last != std::string::npos && last + 1 < raw.size()) { + char& b = raw[last + 1]; + b = (b == 'f') ? 'e' : 'f'; } - return decoded; - }; - std::string raw = b64url_decode(tok); - size_t last = raw.rfind('.'); - if (last != std::string::npos && last + 1 < raw.size()) { - char& b = raw[last + 1]; - b = (b == 'f') ? 'e' : 'f'; + std::string tampered = base64url_encode(raw); + out["token_tamper_rejected"] = json{{"valid", sm.validate_token(tampered, "f", "c")}}; } - std::string tampered = base64url_encode(raw); - out["token_tamper_rejected"] = json{{"valid", sm.validate_token(tampered, "f", "c")}}; - } - // wire_validate_webhook_signature: correct HMAC-SHA1 -> valid. - const std::string wh_url = "https://example.com/hook"; - const std::string wh_body = R"({"event":"call.created"})"; - out["wire_validate_webhook_signature"] = - json{{"valid", ValidateWebhookSignature(kSecret, oracle_sig(wh_url, wh_body, kSecret), wh_url, - wh_body)}}; + // wire_validate_webhook_signature: correct HMAC-SHA1 -> valid. + const std::string wh_url = "https://example.com/hook"; + const std::string wh_body = R"({"event":"call.created"})"; + out["wire_validate_webhook_signature"] = + json{{"valid", ValidateWebhookSignature(kSecret, oracle_sig(wh_url, wh_body, kSecret), + wh_url, wh_body)}}; - // wire_validate_webhook_signature_bad: wrong sig -> invalid. - { - std::string bad; - for (int i = 0; i < 8; ++i) bad += "deadbeef"; - out["wire_validate_webhook_signature_bad"] = - json{{"valid", ValidateWebhookSignature(kSecret, bad, wh_url, wh_body)}}; - } + // wire_validate_webhook_signature_bad: wrong sig -> invalid. + { + std::string bad; + for (int i = 0; i < 8; ++i) { + bad += "deadbeef"; + } + out["wire_validate_webhook_signature_bad"] = + json{{"valid", ValidateWebhookSignature(kSecret, bad, wh_url, wh_body)}}; + } - // wire_redact_url: credentials + token redacted, structure preserved. - out["wire_redact_url"] = - json{{"redacted", RedactUrl("https://user:s3cr3t@api.signalwire.com/path?token=abc")}}; + // wire_redact_url: credentials + token redacted, structure preserved. + out["wire_redact_url"] = + json{{"redacted", RedactUrl("https://user:s3cr3t@api.signalwire.com/path?token=abc")}}; - // wire_filter_sensitive_headers: authorization + x-api-key dropped, - // content-type kept. - { - std::map headers = { - {"Authorization", "Bearer x"}, {"X-Api-Key", "y"}, {"Content-Type", "application/json"}}; - auto filtered = FilterSensitiveHeaders(headers); - json fj = json::object(); - for (const auto& [k, v] : filtered) fj[k] = v; - out["wire_filter_sensitive_headers"] = json{{"filtered", fj}}; - } + // wire_filter_sensitive_headers: authorization + x-api-key dropped, + // content-type kept. + { + std::map headers = { + {"Authorization", "Bearer x"}, {"X-Api-Key", "y"}, {"Content-Type", "application/json"}}; + auto filtered = FilterSensitiveHeaders(headers); + json fj = json::object(); + for (const auto& [k, v] : filtered) { + fj[k] = v; + } + out["wire_filter_sensitive_headers"] = json{{"filtered", fj}}; + } - std::cout << out.dump() << "\n"; - return 0; + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } } diff --git a/tools/wire_relay_dump.cpp b/tools/wire_relay_dump.cpp index f27fa92..2949068 100644 --- a/tools/wire_relay_dump.cpp +++ b/tools/wire_relay_dump.cpp @@ -145,7 +145,9 @@ class MockRelay { // the porting-sdk free-port contract for mock-binding harnesses). int pick_free_port() { int sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock < 0) return 0; + if (sock < 0) { + return 0; + } sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); @@ -233,213 +235,225 @@ void decode_events(json& out) { } // namespace int main() { - if (!std::getenv("RELAY_DUMP_DEBUG")) { - signalwire::Logger::instance().suppress(); - } - ix::initNetSystem(); + // exception-escape guard: main() must not let an exception escape + // (that is std::terminate, with no message). Report and exit nonzero. + try { + if (!std::getenv("RELAY_DUMP_DEBUG")) { + signalwire::Logger::instance().suppress(); + } + ix::initNetSystem(); - auto mock = std::make_shared(); + auto mock = std::make_shared(); - // Bind the mock WS server on a self-picked free loopback port. - int port = pick_free_port(); - if (port == 0) { - std::cerr << "wire-relay-dump: could not pick a free port\n"; - return 1; - } - ix::WebSocketServer server(port, "127.0.0.1"); - server.setOnClientMessageCallback( - [mock, &server](const std::shared_ptr& /*state*/, ix::WebSocket& ws, - const ix::WebSocketMessagePtr& msg) { - if (msg->type == ix::WebSocketMessageType::Open) { - if (std::getenv("RELAY_DUMP_DEBUG")) std::cerr << "[mock] client opened\n"; - // Track this client so the mock can push server-initiated events. - for (auto& client : server.getClients()) { - if (client.get() == &ws) { - mock->register_client(client); - break; + // Bind the mock WS server on a self-picked free loopback port. + int port = pick_free_port(); + if (port == 0) { + std::cerr << "wire-relay-dump: could not pick a free port\n"; + return 1; + } + ix::WebSocketServer server(port, "127.0.0.1"); + server.setOnClientMessageCallback( + [mock, &server](const std::shared_ptr& /*state*/, ix::WebSocket& ws, + const ix::WebSocketMessagePtr& msg) { + if (msg->type == ix::WebSocketMessageType::Open) { + if (std::getenv("RELAY_DUMP_DEBUG")) { + std::cerr << "[mock] client opened\n"; + } + // Track this client so the mock can push server-initiated events. + for (auto& client : server.getClients()) { + if (client.get() == &ws) { + mock->register_client(client); + break; + } + } + } else if (msg->type == ix::WebSocketMessageType::Message) { + if (std::getenv("RELAY_DUMP_DEBUG")) { + std::cerr << "[mock] recv: " << msg->str.substr(0, 120) << "\n"; + } + mock->on_message(ws, msg->str); + } else if (msg->type == ix::WebSocketMessageType::Error) { + if (std::getenv("RELAY_DUMP_DEBUG")) { + std::cerr << "[mock] error: " << msg->errorInfo.reason << "\n"; } } - } else if (msg->type == ix::WebSocketMessageType::Message) { - if (std::getenv("RELAY_DUMP_DEBUG")) { - std::cerr << "[mock] recv: " << msg->str.substr(0, 120) << "\n"; - } - mock->on_message(ws, msg->str); - } else if (msg->type == ix::WebSocketMessageType::Error) { - if (std::getenv("RELAY_DUMP_DEBUG")) - std::cerr << "[mock] error: " << msg->errorInfo.reason << "\n"; - } - }); - - server.disablePerMessageDeflate(); - auto res = server.listen(); - if (!res.first) { - std::cerr << "wire-relay-dump: listen failed: " << res.second << "\n"; - return 1; - } - server.start(); - // Give the accept loop a moment to come up before the client dials in. - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (std::getenv("RELAY_DUMP_DEBUG")) std::cerr << "[mock] listening on port " << port << "\n"; + }); - // Point the client at the mock over plain ws://. - ::setenv("SIGNALWIRE_RELAY_SCHEME", "ws", 1); - std::string host = std::string("127.0.0.1:") + std::to_string(port); + server.disablePerMessageDeflate(); + auto res = server.listen(); + if (!res.first) { + std::cerr << "wire-relay-dump: listen failed: " << res.second << "\n"; + return 1; + } + server.start(); + // Give the accept loop a moment to come up before the client dials in. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (std::getenv("RELAY_DUMP_DEBUG")) { + std::cerr << "[mock] listening on port " << port << "\n"; + } - json out = json::object(); - decode_events(out); + // Point the client at the mock over plain ws://. + ::setenv("SIGNALWIRE_RELAY_SCHEME", "ws", 1); + std::string host = std::string("127.0.0.1:") + std::to_string(port); - relay::RelayClient client("proj-1", "tok-1", host, {"default"}); - if (!client.connect()) { - std::cerr << "wire-relay-dump: client connect failed\n"; - server.stop(); - return 1; - } + json out = json::object(); + decode_events(out); + + relay::RelayClient client("proj-1", "tok-1", host, {"default"}); + if (!client.connect()) { + std::cerr << "wire-relay-dump: client connect failed\n"; + server.stop(); + return 1; + } - // Build a Call bound to the client to drive the verb cases directly (the - // corpus verbs are pure frame builders — an inbound call is not required to - // observe the frame they send). - relay::Call call(kCall, kNode, &client); - - // relay_play - call.play(json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 5.0, - kCid); - settle(); - out["relay_play"] = frame("calling.play", mock->last_frame("calling.play")); - - // relay_play_tts — play_tts(text, language, gender, voice, volume); the corpus - // sets voice, so pass it as the 4th positional (gender stays empty). - call.play_tts("Hello world", "", "", "en-US-Neural"); - settle(); - out["relay_play_tts"] = frame("calling.play", mock->last_frame("calling.play")); - - // relay_record - call.record(json{{"audio", {{"format", "mp3"}, {"beep", true}}}}, kCid); - settle(); - out["relay_record"] = frame("calling.record", mock->last_frame("calling.record")); - - // relay_connect - call.connect( - json::array( - {json::array({{{"type", "phone"}, {"params", {{"to_number", "+15551112222"}}}}})}), - json{{"ringback", json::array({{{"type", "ringtone"}, {"params", {{"name", "us"}}}}})}, - {"tag", "leg-1"}, - {"max_duration", 3600}}); - settle(); - out["relay_connect"] = frame("calling.connect", mock->last_frame("calling.connect")); - - // relay_collect - call.collect(json{{"digits", {{"max", 4}, {"terminators", "#"}}}, - {"speech", {{"language", "en-US"}}}, - {"initial_timeout", 5.0}, - {"partial_results", true}}, - kCid); - settle(); - out["relay_collect"] = frame("calling.collect", mock->last_frame("calling.collect")); - - // relay_prompt (play_and_collect) — prompt_tts(text, collect, language, gender, - // voice, volume); pass voice as the 5th positional (gender empty). - call.prompt_tts("Enter your PIN", json{{"digits", {{"max", 4}}}}, "", "", "en-US-Neural"); - settle(); - out["relay_prompt"] = - frame("calling.play_and_collect", mock->last_frame("calling.play_and_collect")); - - // relay_detect — detect(params, control_id) merges params into the frame; the - // detect descriptor is nested under "detect" with a sibling top-level timeout. - call.detect(json{{"detect", {{"type", "machine"}, {"params", {{"initial_timeout", 4.0}}}}}, - {"timeout", 30.0}}, + // Build a Call bound to the client to drive the verb cases directly (the + // corpus verbs are pure frame builders — an inbound call is not required to + // observe the frame they send). + relay::Call call(kCall, kNode, &client); + + // relay_play + call.play(json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 5.0, kCid); - settle(); - out["relay_detect"] = frame("calling.detect", mock->last_frame("calling.detect")); - - // relay_detect_amd - call.detect_answering_machine(json{{"initial_timeout", 4.0}, {"machine_words_threshold", 6}}, - 30.0); - settle(); - out["relay_detect_amd"] = frame("calling.detect", mock->last_frame("calling.detect")); - - // relay_tap - call.tap(json{{"tap", {{"type", "audio"}, {"params", {{"direction", "both"}}}}}, - {"device", {{"type", "ws"}, {"params", {{"uri", "wss://x/tap"}}}}}}, - kCid); - settle(); - out["relay_tap"] = frame("calling.tap", mock->last_frame("calling.tap")); - - // relay_send_fax - call.send_fax("https://x/doc.pdf", "Hdr", "+15550001111", kCid); - settle(); - out["relay_send_fax"] = frame("calling.send_fax", mock->last_frame("calling.send_fax")); - - // relay_live_transcribe — the caller's action descriptor MUST be wrapped as - // params.action on the wire (schema requires it); this pins that shape. - call.live_transcribe(json{{"start", {{"lang", "en"}}}}); - settle(); - out["relay_live_transcribe"] = - frame("calling.live_transcribe", mock->last_frame("calling.live_transcribe")); - - // relay_live_translate — same action-wrap contract, plus the optional - // sibling status_url param. - call.live_translate(json{{"start", {{"from_lang", "en"}, {"to_lang", "es"}}}}, "https://x/cb"); - settle(); - out["relay_live_translate"] = - frame("calling.live_translate", mock->last_frame("calling.live_translate")); - - // ---- control-ops (Action methods) ---- - { - auto pa = call.play( - json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); settle(); - pa.stop(); + out["relay_play"] = frame("calling.play", mock->last_frame("calling.play")); + + // relay_play_tts — play_tts(text, language, gender, voice, volume); the corpus + // sets voice, so pass it as the 4th positional (gender stays empty). + call.play_tts("Hello world", "", "", "en-US-Neural"); settle(); - out["relay_play_stop"] = frame("calling.play.stop", mock->last_frame("calling.play.stop")); - } - { - auto pa = call.play( - json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); + out["relay_play_tts"] = frame("calling.play", mock->last_frame("calling.play")); + + // relay_record + call.record(json{{"audio", {{"format", "mp3"}, {"beep", true}}}}, kCid); settle(); - pa.pause("silence"); + out["relay_record"] = frame("calling.record", mock->last_frame("calling.record")); + + // relay_connect + call.connect( + json::array( + {json::array({{{"type", "phone"}, {"params", {{"to_number", "+15551112222"}}}}})}), + json{{"ringback", json::array({{{"type", "ringtone"}, {"params", {{"name", "us"}}}}})}, + {"tag", "leg-1"}, + {"max_duration", 3600}}); settle(); - out["relay_play_pause"] = frame("calling.play.pause", mock->last_frame("calling.play.pause")); - } - { - auto ra = call.record(json{{"audio", {{"format", "mp3"}}}}, kCid); + out["relay_connect"] = frame("calling.connect", mock->last_frame("calling.connect")); + + // relay_collect + call.collect(json{{"digits", {{"max", 4}, {"terminators", "#"}}}, + {"speech", {{"language", "en-US"}}}, + {"initial_timeout", 5.0}, + {"partial_results", true}}, + kCid); settle(); - ra.resume(); + out["relay_collect"] = frame("calling.collect", mock->last_frame("calling.collect")); + + // relay_prompt (play_and_collect) — prompt_tts(text, collect, language, gender, + // voice, volume); pass voice as the 5th positional (gender empty). + call.prompt_tts("Enter your PIN", json{{"digits", {{"max", 4}}}}, "", "", "en-US-Neural"); settle(); - out["relay_record_resume"] = - frame("calling.record.resume", mock->last_frame("calling.record.resume")); - } - { - auto pa = call.play( - json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); + out["relay_prompt"] = + frame("calling.play_and_collect", mock->last_frame("calling.play_and_collect")); + + // relay_detect — detect(params, control_id) merges params into the frame; the + // detect descriptor is nested under "detect" with a sibling top-level timeout. + call.detect(json{{"detect", {{"type", "machine"}, {"params", {{"initial_timeout", 4.0}}}}}, + {"timeout", 30.0}}, + kCid); settle(); - pa.volume(3.5); + out["relay_detect"] = frame("calling.detect", mock->last_frame("calling.detect")); + + // relay_detect_amd + call.detect_answering_machine(json{{"initial_timeout", 4.0}, {"machine_words_threshold", 6}}, + 30.0); settle(); - out["relay_play_volume"] = - frame("calling.play.volume", mock->last_frame("calling.play.volume")); - } + out["relay_detect_amd"] = frame("calling.detect", mock->last_frame("calling.detect")); + + // relay_tap + call.tap(json{{"tap", {{"type", "audio"}, {"params", {{"direction", "both"}}}}}, + {"device", {{"type", "ws"}, {"params", {{"uri", "wss://x/tap"}}}}}}, + kCid); + settle(); + out["relay_tap"] = frame("calling.tap", mock->last_frame("calling.tap")); + + // relay_send_fax + call.send_fax("https://x/doc.pdf", "Hdr", "+15550001111", kCid); + settle(); + out["relay_send_fax"] = frame("calling.send_fax", mock->last_frame("calling.send_fax")); + + // relay_live_transcribe — the caller's action descriptor MUST be wrapped as + // params.action on the wire (schema requires it); this pins that shape. + call.live_transcribe(json{{"start", {{"lang", "en"}}}}); + settle(); + out["relay_live_transcribe"] = + frame("calling.live_transcribe", mock->last_frame("calling.live_transcribe")); + + // relay_live_translate — same action-wrap contract, plus the optional + // sibling status_url param. + call.live_translate(json{{"start", {{"from_lang", "en"}, {"to_lang", "es"}}}}, "https://x/cb"); + settle(); + out["relay_live_translate"] = + frame("calling.live_translate", mock->last_frame("calling.live_translate")); + + // ---- control-ops (Action methods) ---- + { + auto pa = call.play( + json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); + settle(); + pa.stop(); + settle(); + out["relay_play_stop"] = frame("calling.play.stop", mock->last_frame("calling.play.stop")); + } + { + auto pa = call.play( + json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); + settle(); + pa.pause("silence"); + settle(); + out["relay_play_pause"] = frame("calling.play.pause", mock->last_frame("calling.play.pause")); + } + { + auto ra = call.record(json{{"audio", {{"format", "mp3"}}}}, kCid); + settle(); + ra.resume(); + settle(); + out["relay_record_resume"] = + frame("calling.record.resume", mock->last_frame("calling.record.resume")); + } + { + auto pa = call.play( + json::array({{{"type", "audio"}, {"params", {{"url", "https://x/a.mp3"}}}}}), 0.0, kCid); + settle(); + pa.volume(3.5); + settle(); + out["relay_play_volume"] = + frame("calling.play.volume", mock->last_frame("calling.play.volume")); + } + + // ---- RelayClient-level frames ---- + // relay_client_execute + client.execute("calling.answer", json{{"node_id", kNode}, {"call_id", kCall}}); + settle(); + out["relay_client_execute"] = frame("calling.answer", mock->last_frame("calling.answer")); + + // relay_send_message + client.send_message("+15553334444", "+15551112222", "hi", {}, {"t1"}); + settle(); + out["relay_send_message"] = frame("messaging.send", mock->last_frame("messaging.send")); - // ---- RelayClient-level frames ---- - // relay_client_execute - client.execute("calling.answer", json{{"node_id", kNode}, {"call_id", kCall}}); - settle(); - out["relay_client_execute"] = frame("calling.answer", mock->last_frame("calling.answer")); - - // relay_send_message - client.send_message("+15553334444", "+15551112222", "hi", {}, {"t1"}); - settle(); - out["relay_send_message"] = frame("messaging.send", mock->last_frame("messaging.send")); - - // relay_dial - client.dial(json::array({json::array( - {{{"type", "phone"}, {"params", {{"to_number", "+15551112222"}}}}})}), - "dial-1", 3000, 600); - settle(); - out["relay_dial"] = frame("calling.dial", mock->last_frame("calling.dial")); - - client.disconnect(); - server.stop(); - ix::uninitNetSystem(); - - std::cout << out.dump() << "\n"; - return 0; + // relay_dial + client.dial(json::array({json::array( + {{{"type", "phone"}, {"params", {{"to_number", "+15551112222"}}}}})}), + "dial-1", /*max_duration=*/600, /*dial_timeout=*/3.0); + settle(); + out["relay_dial"] = frame("calling.dial", mock->last_frame("calling.dial")); + + client.disconnect(); + server.stop(); + ix::uninitNetSystem(); + + std::cout << out.dump() << "\n"; + return 0; + } catch (const std::exception& e) { + std::cerr << "fatal: " << e.what() << "\n"; + return 1; + } }