feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the C++ SDK#25
Merged
Conversation
…-base) Remove the entire Twilio-compat surface from signalwire-cpp and lay the item-B adoption foundation. Item A (generator, 47/47) is committed-adjacent (untracked generator from the A turn is included here). 9th port. The destructive item-B adopt/delete + item E follow as one atomic turn. Item C — deleted 12 Compat* structs + CompatNamespace from rest_client.hpp (~310 lines), the compat() accessor/member/construction, 9 test_rest_mock_compat_*.cpp + includes, 78 rest_mock_cov_compatibility_* TEST blocks (286->208), the route_registry compat block, compat in enumerate_surface.py (CLASS_MODULE_MAP + CLASS_RENAME_MAP), the stale compatibility.* REST_COVERAGE_GAPS entry, 35 PORT_ADDITIONS compat entries, DOC_AUDIT_IGNORE compat block, rest/docs/compat.md + rest/examples/rest_compat_laml.cpp, README/CHECKLIST refs. Namespace count 21->20. Item-B foundation — base_resource.hpp (BaseResource/ReadResource/CrudResource w/ 3-arg PUT/PATCH verb dispatch/FabricResource + list_addresses); all 56 emitted generated headers compile against it. Generator now emits generated_surface_map.json (byte-identical 53-class map to ruby's). Verified: compat GONE from port_surface.json + port_signatures.json (regenerated — the agent had left them stale, caught + regenerated); SPEC-PARITY 209/228 0 B-A divergences (was 78 compat); REST-COVERAGE exit 0 (only the predicted fabric.list_sip_gateway_addresses gap); build GREEN; 315/315 mock tests pass; GEN-FRESH matches. No BACKLOG tags; 0 not_yet added (the 902 pre-existing are un-ported subsystems D/H/I). DRIFT red = pre-existing unadopted state (hand classes map to non-existent oracle modules; 0 of 747 compat-related) — cleared by the next item-B adoption turn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…e + E) Complete cpp's REST-resource adoption: emit + adopt the generated resources, wire the adapter, generate the full-mock wire-test suite (E), and delete the hand resource/coverage classes. Builds on the C+foundation commit (9d7cb94). Item E — scripts/generate_rest_tests.py (reflection-free): extended tools/route_registry.cpp with a CALL(via,expr) macro capturing each generated- client call's source + the wire (method,path); the Python generator joins that plan × spec operationId via the mock's SpecLoader (longest-template-wins). 12 test_rest_generated_<ns>.cpp = 418 tests (209 routes × success+error), GEN-FRESH-TESTS-gated, rest_mock_gen_* prefix (coverage-filter + parallel isolation). Item B adopt+delete — RestClient composes the generated ResourceTree (§8); 20 hand namespace ptrs + init_namespaces() gone; deleted test_rest_full_coverage.cpp (208 hand tests); rewrote 7 behavioral test files to the generated API preserving wire assertions. Adapter: surface-map projection (53 entries, both enumerators) + SIDECAR unfold + container-member + base-class projection (inherited write-verb sidecar records 0-mismatch to the oracle's per-base declared set); del->delete rename; GEN-FRESH + GEN-FRESH-TESTS wired. Verified (JSONs regenerated): SURFACE-DIFF resource/client-tree/base/RestClient 0; hand extras gone; SIG DRIFT 760->394 (all REST-adoption drift 0; residual = item-D gen-payload + pre-existing non-REST); SPEC-PARITY 209 green; REST-COVERAGE before FAIL 208/228 (hand suite never hit fabric.list_sip_gateway_addresses) -> after GREEN 209/228 (generated suite closed it); full suite 1688/1688 pass; build + NO-CHEAT clean; L11 route registry 209. No BACKLOG tags (the 892 PORT_OMISSIONS are pre-existing un-ported subsystems D/H/I). No wire bugs (hand REST fully deleted; generated = single spec-derived source). L20 parser find: the regex surface enumerator treated /* inside a // comment (generated/*.hpp) as a block-comment opener, swallowing the RestClient class — fixed source-neutrally by rewording the comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…-fresh/swaig gates [item D] Mirrors the ruby template: 13 REST <ns>_types_generated (753), swml_verbs (155), relay protocol_types (123), post_prompt (14), swaig_request (2), swaig_actions (4) — all 0 miss / 0 extra vs the surface oracle, all four generators deterministic (GEN-FRESH). Enumerator routes each gen-type namespace to its oracle module by path and registers method-less structs at scope-open. port_signatures.json byte-identical (libclang drops zero-method structs, matching the reference sig-oracle). 90 C++-keyword field escapes as adapter renames (wire key preserved); ~1053 headers compile clean under one TU. Wires SWAIG-COVERAGE (cpp 25/27, 2 signed-off) + 3 GEN-FRESH type gates into run-ci.sh. No _resources_generated regression. Item-I subsystem tail (skills/web/utils) remains out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…formatter (clang-format is now a no-op) Replaces the per-run clang-format shell-out with format_generated_cpp() in scripts/_cpp_fmt.py — a pure-Python formatter that reproduces clang-format 18 byte-for-byte (verified: diff -r empty across all 1105 generated headers; the tree is byte-identical to HEAD; formatter idempotent). clang_format_source remains only as a verify-only backstop. GEN-FRESH and the FMT gate now both pass by construction instead of being mutually exclusive. Systematic template defects fixed (8007 diff-segments -> 0): include-ordering (nlohmann/json.hpp merged into the sorted <...> group), body/struct indentation (6-space -> 4, AccessModifierOffset -1), short-if/short-function/bin-pack wrapping, blank-line collapse — in the shared struct emitter + REST gen_header. Fixes a prior-turn bug: generated_surface_map.json was being clang-formatted as if it were C++ at 0f790cc, mangling its JSON layout. The format pass now targets only .hpp/.h and emits .json sidecars verbatim; the map is de-mangled (content byte-identical, valid JSON). Known residual: one long AUTO-GENERATED comment line is reflowed by the Python formatter rather than emitted pre-wrapped by the template — the formatter handles it deterministically, so both gates stay green; a template-level pre-wrap is a minor follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…apping _env.sh
scripts/_env.sh resolves REPO from ${BASH_SOURCE[0]} (never $PWD) and prepends
llvm@18 to PATH so clang-format/clang-tidy AND the Python generators (_cpp_fmt.py
via shutil.which) resolve clang-18; fails loud if clang-format isn't major 18.
run-{format,lint,tests}.sh work from ANY CWD; run-ci FMT/LINT/TEST delegate to them.
CLAUDE.md + README point at the scripts. Stages a pre-existing clang-format nit in
tools/route_registry.cpp (hand-written, not generated) so CI FMT --check is green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
SURFACE-DIFF exit 0 (2290 symbols; 24 impossible: / 66 approved:) from 215 omitted + 180 not_yet_implemented banned tags. NO-LAUNDER: 0 banned. FMT clean; TESTS 1863/0. DRIFT 869(HEAD) -> 378 (improved), residual is the pre-existing item-D gen-payload gap only (0 non-gen-payload drifts remain). Implemented ~50 real C++ methods+classes with behavioral tests (core infra / SWML+web / relay+sessions+prefabs, mirroring java's AOT disposition). Projected mixins / free-fns / relay-action-subclasses / __call__ / __init__ via the enumerator (reconciled, never omitted). Tagged 24 genuinely-impossible (decorators/dunders/FastAPI-forms/serverless) + MCPGatewaySkill->approved; 143 H/I signature idioms in PORT_SIGNATURE_OMISSIONS, 43 PORT_ADDITIONS. Fixed a latent enumerator bug (FREE_FUNCTION_RENAMES defined-but-unused). Residual (out of H/I scope, honestly UNTAGGED not silenced): the 378 DRIFT entries are all the gen-payload SWML-payload gap — the oracle records AIParams/AIObject/UserSWAIGFunction/ Webhook fields as property-getter METHODS, C++ implements them as std::optional<> FIELDS on POD structs the libclang sig-enumerator doesn't emit as methods. Fix = project those fields as property-getters in enumerate_signatures.py (a follow-up, faster after the cpp perf work). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…ache [Tier 1.1/1.2/C1] enumerate_signatures.py parsed each of ~1000+ headers as a SEPARATE libclang TU, re-parsing the macOS SDK + libc++ every time. Now: synthesize ONE umbrella unsaved-file that #includes every header, parse once (#pragma once collapses repeats; the existing walk_translation_unit file_filter keeps only decls under include/). Plus PARSE_SKIP_FUNCTION_BODIES (reads decls only) and dropped PARSE_DETAILED_PROCESSING_RECORD (unused). Per-header loop kept as fallback. port_signatures.json BYTE-IDENTICAL (sha256 verified across HEAD / old regen / new regen; 81 modules / 1211 methods) — pure speedup. De-dup: SURFACE-FRESH caches its fresh port_surface.json to .sw-tmp (gitignored, not /tmp); SURFACE-DIFF reuses it (regen fallback). ccache: CMakeLists availability-gated COMPILER_LAUNCHER — no-op when absent (build works, 62s), warm cold 66s->0.8s (109 hits); declared in _env.sh + porting-sdk cross-port.yml. sw-verify cpp: SURFACE-DIFF + NO-LAUNDER PASS, zero new failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…T 378 -> 0
The generated read-side payload structs (core/{swml_verbs,post_prompt,swaig_request}_
generated/*.hpp) are method-less PODs — one std::optional<T> FIELD per snake wire key.
libclang only surfaces method/ctor cursors, so each struct was dropped entirely
(if not methods_out: continue), and the Python oracle's zero-arg property-getters for
those same wire keys all read as missing-port DRIFT.
_project_gen_payload_getters() (after _apply_rest_sidecar) iterates
enumerate_surface.GENERATED_PAYLOAD_NS (imported, not hardcoded — new payload namespaces
covered automatically), reads each struct's public fields honoring the generator's
comment (so default_/enum_ renames land on the oracle name), and projects
those the oracle records as getters into the zero-arg getter shape. Port-only fields the
oracle doesn't surface (the open member + 27 unexposed AIParams keys) are NOT
projected — no invented surface. Signature-side analogue of the surface enumerator's
method-less registration (SURFACE-DIFF was already green).
DRIFT 378 -> 0; SURFACE-DIFF + NO-LAUNDER PASS; 0 banned; non-gen-payload signatures
byte-identical (H/I not regressed). cpp now at full A-I parity, DRIFT clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
a6eba0b to
6e280ee
Compare
… -> warm) CMakeLists.txt already gates ccache as the compiler launcher when present, but the per-port test.yml never installed ccache nor persisted its store — so every cold CI checkout recompiled the whole C++ project (+ the separate clang build-tidy/ tree for LINT) from scratch, the bulk of cpp's ~14min CI. Install ccache + actions/cache on ~/.cache/ccache keyed by source hash. Both build/ (g++) and build-tidy/ (clang) share the store. Measured locally: cold 12s -> warm 5s (82% hit); the win is proportionally larger on the slow 2-vCPU CI runner. Availability-gated: absent ccache is a strict no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…ve-const (79 findings) Applied clang-tidy --fix for the mechanical curated checks (readability-redundant-string-init, readability-braces-around-statements, performance-move-const-arg, readability-redundant-casting) across the tree. Cleared 79 of ~94 LINT findings introduced by the H/I RELAY work — the LINT gate wasn't exercised while cpp CI was broken on the missing scheduler. Builds clean (g++) + all 1863 tests pass. 13 findings needing judgment (optional-access guards, string-concat, exception-escape, a false-positive macro-parens) handled in the follow-up commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…class-name macro - security_config: *ssl_cert_path_/*ssl_key_path_ -> .value() (checked access; both are guaranteed present past validate_ssl_config, but make it explicit not UB-on-bug). - swml_handler: the else branch dereferenced *prompt_pom unconditionally — guard with has_value() so neither-set input can't deref an empty optional (real robustness fix). - action.hpp: NOLINT the bugprone-macro-parentheses on SIGNALWIRE_RELAY_ACTION_SUBCLASS — NAME is a class name (`class NAME : public Action`) and cannot be parenthesized. Builds clean + 1863/1863 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…scape) - web_service: contain handler exceptions (base64_decode on malformed auth header could throw into httplib's worker thread) -> try/catch returns 500; explicit move for blocked_extensions_ (the ternary-with-const-default silently dropped the move); NOLINT the intentionally-ignored config_file parity param. - concierge: reserve() before the name-collection loop; NOLINT the cold reply-string builds. - swaig_function: NOLINT the cold validation-error string concat. Builds clean, format-clean, 1863/1863 tests pass. cpp LINT gate now green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Final batch on top of the autofix: rewrote cold message-concats to +=, guarded optional access (has_value not .value()), config_file -> const-ref (not in surface), contain handler exceptions via named-handler try/catch(...) with a placeable NOLINT, reserve()+move fixes. cpp LINT now exits 0; SURFACE-DIFF/DRIFT/GEN-FRESH still pass; 1863/1863 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…-TU fan-out) The LINT gate ran ONE clang-tidy invocation over all 65 .cpp TUs serially — measured as the ACTUAL cpp CI wall-clock driver (~824s of a 1077s run; the build/TEST gate is seconds). ccache does NOT touch this (it's static analysis, not compilation). Fan one clang-tidy per file across cores via find -print0 | xargs -0 -P: identical behavior (same binary, -p DB, --header-filter, .clang-tidy WarningsAsErrors so any finding still fails; xargs exits 123 on any nonzero child). bash-3.2 compatible (no mapfile). Local: serial 74s -> parallel 20s (~3.7x, 8 cores); 0 findings preserved. Overridable via SWCPP_TIDY_JOBS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…what changed The LINT gate (clang-tidy) is cpp's CI wall-clock driver even after the xargs fan-out (~252s of the 478s run). ccache can't touch it (static analysis, not compilation) — that dead ccache cache-persistence is removed here. Instead: vendor matus-chochlik/ctcache's single self-contained client (scripts/clang_tidy_cache.py, pinned @0c7fee2, Boost license, stdlib-only in local mode) and route clang-tidy through it in run-lint.sh, availability- gated on CTCACHE_DIR + wrapper + python3 (else exact prior behavior). It hashes the preprocessed TU (headers inlined) + raw #include lines + resolved --dump-config, so a change to a file, ANY header it includes, the checks, or the config invalidates and re-runs; an unchanged TU returns the cached result without running clang-tidy. Audited + tested the safety-critical paths: fail-open (any hash/cache error -> real clang-tidy; a finding is never skipped), cached failures replay their finding+exit code, and edits/header-changes invalidate (verified with a live redundant-string-init probe). Local: cold 22s -> warm 3s (~7x); no-op fallback (CTCACHE_DIR unset) 18s, 0 findings, prior behavior. CI persists ~/.cache/ctcache via actions/cache (stable key + run_id). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…f: always) The combined actions/cache post-save is SKIPPED when a prior step fails — and every cpp run is currently red on the pre-existing spec-coupling, so the ctcache store never persisted (verified: run 1's 'Post Restore clang-tidy cache' step = skipped, restore = cache-not-found forever). Split into actions/cache/restore + a separate actions/cache/save with if: always() so the cache the LINT gate populated is saved regardless of gate outcome. Without this the whole ctcache change is a permanent cold-cache no-win. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Re-verify against current porting-sdk main (f8d45b0) after ports were generated against an earlier SDK. - DRIFT: AIConfigMixin.set_prompt_llm_params / set_post_prompt_llm_params are `**params` var_keyword in the Python reference. Per #58 the oracle now strips the var_keyword tail (records `['self']`), and a trailing var_keyword-tail port param is excused only when required:false. The AgentBase declarations took a non-defaulted `const json& params`, so the enumerator recorded params as required:true -> drift. Give both decls a `= json::object()` default (faithful projection of an optional **params; matches the Bedrock overrides' existing idiom) so params is required:false and the tail is excused. Regenerated port_signatures.json. - SURFACE-FRESH: committed port_surface.json was stale (BedrockAgent / AgentServer / PromptManager methods absent, 698 lines behind a fresh regen). Regenerated it against the current headers + porting-sdk enumerator. - DOC-AUDIT: `del` (the CrudResource/HttpClient DELETE verb; `delete` is a C++ keyword) is documented correctly in rest/docs but the parity surface records the Python name `delete` (enumerate_surface del->delete rename), so it doesn't resolve against port_surface.json. Added `del` to DOC_AUDIT_IGNORE.md with that rationale. Gates: DRIFT, SURFACE-DIFF, SURFACE-FRESH, GEN-FRESH*, GEN-FRESH-TESTS, SPEC-PARITY, EMISSION, TEST (1863/1863), REST-COVERAGE, SWAIG-CLI, SWAIG-COVERAGE, DOC-AUDIT, SKILL-CONTRACT, NO-CHEAT, NO-LAUNDER, FMT, LINT all PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename GEN-FRESH-RELAY-PROTO → GEN-FRESH-RELAY, GEN-FRESH-SWAIG-PAYLOADS → GEN-FRESH-SWAIG, GEN-FRESH-SWML-VERBS → GEN-FRESH-SWML to match the cross-port canonical gate names (java/ruby/perl/rust/dotnet). Same gates, same --check; names only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
run-ci.sh wrote the surface-freshness snapshot (committed_surface[_diff].json) and the REST-COVERAGE / mock-server logs to /tmp — machine-wide scratch that invites cross-run pollution and leftover files. Redirected all host-side scratch to the repo-local, gitignored .sw-tmp/ (mkdir -p once after PORT_ROOT is set). Container- internal paths (docker HOME=/tmp, cpp's in-container run-ci-build) are left as-is — those are the container's ephemeral fs, not the host. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The signature oracle now requires as_router() returning HostAppRouter (the "embed my routes in a host app" capability) on WebMixin + SWMLService. cpp had marked it impossible: "httplib directly, no router object to hand out" — but that was a type-shaped excuse, not a real limit: in cpp-httplib an httplib::Server IS the mountable route-handler unit. Implement swml::Service::as_router() returning a std::shared_ptr<httplib::Server> pre-populated with the service's routes via the same setup_routes() logic serve() uses, but NOT bound to any port — the caller embeds it (listen on it, front it with TLS, or lift its handlers into a parent server). AgentBase inherits it and its /post_prompt, /mcp, /debug routes come along because setup_routes is now virtual (AgentBase's override is dispatched via as_router()/serve()). - swml::Service::as_router() → shared_ptr<httplib::Server> (mountable, unbound) - setup_routes() made virtual so as_router() picks up AgentBase's fuller route set - as_router projected onto WebMixin in enumerate_surface MIXIN_PROJECTIONS - deleted the two false impossible: as_router omissions (WebMixin + SWMLService) - regenerated port_signatures.json + port_surface.json (as_router → HostAppRouter) - 2 tests: mountable-unit shape + mount-on-free-port and dispatch /health + /swaig Type map (in porting-sdk/type_aliases.yaml, committed separately): httplib::Server → class:signalwire.core.web.HostAppRouter Gates: DRIFT PASS, SURFACE-DIFF PASS, NO-LAUNDER PASS, TEST 1865/1865. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Pass 1 webhook reconcile (HIDDEN_SURFACE_AUDIT) against the oracle that now requires the framework-free webhook-validation decision core webhook_middleware.validate(method,url,headers,body,*,signing_key) -> optional<(status,headers,body)>. - Add signalwire::security::Validate free function: the decomposed, framework-free core (no httplib dep) that pulls the signature header (X-SignalWire-Signature or the X-Twilio-Signature alias, case-insensitive) and returns nullopt on pass / a (403, headers, "Forbidden") triple on reject, never leaking which branch failed. - Map it in the enumerator via FREE_FUNCTION_RENAMES (signalwire::security::Validate -> webhook_middleware.validate) and mark the trailing signing_key param keyword-only to match the oracle (C++ has no keyword-only params; call-site sugar, not a contract divergence). - Return type spelled structurally (not the ValidationResponse alias) so the enumerator resolves optional<tuple<int,dict<string,string>,string>>. - The cpp-httplib WrapWithSignatureValidation adapter stays a PORT_ADDITION idiom on top of the core; the make_webhook_validation_dependency FastAPI WRAPPER stays idiom (rationale refreshed to point at the reconciled core). - validate_request / validate_webhook_signature keep their std::variant -> Union type-map reconcile (cpp-typed-overload), unchanged. - Add 6 tests for the decomposed core: valid->nullopt, bad/missing sig->403 triple, X-Twilio-Signature alias honored, case-insensitive header lookup, empty signing_key throws. DRIFT + SURFACE-DIFF PASS; full run-ci PASS (21 gates). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Oracle fba1b19 promoted webhook_middleware.validate to a SURFACE symbol (previously signature-only), so SURFACE-DIFF now requires it as a public module function, not just a signature match. - Project the C++ Validate free function (webhook_validator.hpp, signalwire::security) onto webhook_middleware.validate in the surface enumerator's MODULE_FUNCTION_PROJECTIONS (grep-based, fail-honest — only surfaces when the header actually defines the function). The bare `Validate(` grep does not match ValidateWebhookSignature/ValidateRequest. - Regenerate port_surface.json to include the symbol. sw-verify: DRIFT + SURFACE-DIFF PASS. Full run-ci PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Pass 3 — handle_request:
- Add the framework-free request-dispatch primitive
handle_request(method, url, headers, body?) -> (status, headers, body)
to BOTH swml::Service and agent::AgentBase (Python parity:
SWMLService.handle_request / AgentBase.handle_request override). Over plain
primitives it runs basic-auth over the header map (401 +
WWW-Authenticate:Basic + {"error":"Unauthorized"} on failure), the
routing-callback check for POST with a non-empty body (307 + Location on a
returned route), then renders SWML (Service -> render_document();
AgentBase -> the request-aware render_swml_for_request path).
- Re-type RoutingCallback from (path, params) to
(const json& body, const std::map<std::string,std::string>& headers) ->
std::string on both classes, matching Python's decomposed
callback_fn(body, headers) -> route | None. The register_routing_callback
bodies only store the callback, so no caller changes were needed;
server::AgentServer's separate GlobalRoutingCallback is unaffected.
- Reconcile: SWMLService.handle_request lands drift-0 via the existing type
aliases (tuple<int,dict<string,string>,string>); AgentBase.handle_request
is a genuine override the Python oracle records only on the base
(override dedup) -> PORT_ADDITIONS.md entry (cpp_override_recorded_on_base).
Pass 6 — type_inference:
- New signalwire::swaig::type_inference free functions (header +
src/swaig/type_inference.cpp): infer_schema(ParameterSchema) -> the 5-tuple
(parameters, required, description, is_typed, has_raw_data), derived from the
typed ParameterSchema params-builder (C++ has no lambda hint-reflection, so —
like the .NET/Ruby ports — it infers from the typed builder, the analog of a
handler's parameter list); and create_typed_handler_wrapper(ToolHandler,
has_raw_data) wrapping a handler to the (args, raw_data) convention.
- Delete the 2 "impossible" omissions from PORT_OMISSIONS.md; project both
functions onto signalwire.core.agent.tools.type_inference via
MODULE_FUNCTION_PROJECTIONS (surface) + FREE_FUNCTION_RENAMES (signatures);
add the cpp_typed_builder_infer signature-omission entries for the
callable->ParameterSchema input divergence (same output tuple).
Tests: tests/test_handle_request.cpp (200 render / 401 bad-auth / 307 routing,
Service + AgentBase, GET-doesn't-route) and tests/test_type_inference.cpp
(infer_schema from a typed builder incl. raw_data exclusion + description
passthrough + untyped-empty; wrapper arg/raw forwarding + normalization) — 14
tests, all passing.
Also brace-fix parameter_schema.hpp single-statement bodies (newly reachable
from a src TU via type_inference.cpp, so now clang-tidy-linted).
Regenerated port_surface.json + port_signatures.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…(#61) The served SWML handler (AgentBase::handle_swml_request and the inline Service::setup_routes swml_handler) re-implemented basic-auth + render but SKIPPED the routing-callback branch, so the decomposed handle_request() core (auth-401 / routing-307 / render-200) had zero production callers and a registered routing callback that returned a redirect produced a 200 over the wire instead of a real 307. Both served handlers now delegate to handle_request(method, url, headers, body) and marshal the returned (status, headers, body) triple back into the framework response, including the 307 Location and the 401 WWW-Authenticate. The framework-specific plumbing (header extraction, body parse) stays as a thin adapter; the auth/routing/render decision comes from handle_request. Adds a served-path test (test_served_routing.cpp) that stands up serve() on an ephemeral port and asserts the actual endpoint returns 307+Location on a routing redirect, 401+WWW-Authenticate on bad auth, and 200 SWML on the happy path. Fails before this change (307 assertion sees 200), passes after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Adds the required Tier-2 behavioral contract tests (porting-sdk/ BEHAVIORAL_CONTRACTS.md #2-#6) to the native suite and fixes each stub the tests expose. Signatures are unchanged (DRIFT/SURFACE-DIFF clean); these are body-level stub fixes the surface gates cannot see. Tests (tests/test_tier2_behavioral.cpp), each forced against its prior stub: - #2 set_prompt/post_prompt_llm_params MERGE (were REPLACE) - #3 InfoGatherer submit_answer STATE (already real; test added) - #4 native_vector_search REMOTE HTTP (real POST to <remote_url>/search) - #5 serverless per-platform DISPATCH (lambda/cgi/gcf + dispatcher) - #6 SIP routing DISPATCH over the served /sip path Stub fixes: - set_prompt_llm_params / set_post_prompt_llm_params: merged instead of replacing the accumulated params (mirrors Python .update()). - native_vector_search: the REGISTERED skill (NativeVectorSearchSkillR in skill_registry.cpp, which shadows the builtin) returned "Search: <q>". Now issues a real POST to <remote_url>/search via skills_http::http_post and formats the {content,score,metadata} results into the FunctionResult. The builtin/native_vector_search.cpp copy is fixed the same way. - serverless: added per-platform dispatch (handle_lambda / handle_gcf / handle_azure / handle_cgi + handle_serverless_request dispatcher) in signalwire::utils, each adapting a platform event/env into AgentBase::handle_request and returning (status, headers, body). Previously detection-only (is_serverless_mode). - SIP: enable_sip_routing now registers a /sip routing callback that extracts the SIP username and consults the registered usernames; setup_routes mounts registered routing-callback paths so the served /sip reaches handle_request (was stored-but-unconsulted -> 404). register_sip_username stores lower-cased; auto_map_sip_usernames derives usernames from name/route (was a no-op flag). Security: - SessionManager::validate_token: compare token function_name/call_id with timing_safe_compare instead of != (was a timing side-channel). sw-verify cpp: all gates PASS. Full run-ci: PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add AgentBase::handle_serverless_request(event={}, context={}, mode="")
returning utils::ServerlessResponse, delegating to the existing free
function signalwire::utils::handle_serverless_request(*this, ...). This
is the oracle-matching instance entry point for
ServerlessMixin.handle_serverless_request; the per-platform
handle_lambda/gcf/azure/cgi dispatchers stay in utils.
The enumerator projection (scripts/enumerate_surface.py) now lists
handle_serverless_request under ServerlessMixin so it matches the oracle
as a method. Removed the two stale PORT_OMISSIONS entries that excused
the missing method (the serverless_out_of_scope vocabulary line and the
ServerlessMixin.handle_serverless_request: impossible line) — the
behavior is real and tested (tests/test_tier2_behavioral.cpp). Added the
standard python_mixin_collapsed PORT_ADDITIONS entry for the AgentBase
copy, matching every other flattened mixin method.
Regenerated port_signatures.json + port_surface.json. DRIFT and
SURFACE-DIFF exit 0 (handle_serverless_request matches as a method with
no omission).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…skills discovery
Contract 7 (#70) — token wire format + nonce parity:
Rewrite SessionManager to Python's exact scheme. A minted token is now the
base64url-encoding of the 5 dot-fields {call_id}.{function_name}.{expiry}.
{nonce}.{signature}; the HMAC-SHA256 signed message is {call_id}:{function_name}:
{expiry}:{nonce}; nonce = 16 hex chars (token_hex(8)); validate/debug decode the
5-field form; call_id-first (was fn-first 3-field base64-payload, no nonce).
Signature + field compares stay constant-time. New contract-7 tests: 5 fields +
non-empty nonce, two mints differ in nonce, python-oracle-format token validates
in-port (interop), tampered sig rejected, constant-time compare.
Contract 8 (#74) — AI/LLM structured add_pattern_hint / add_language:
add_pattern_hint now attaches a STRUCTURED {hint,pattern,replace,ignore_case}
object (was a bare string); hints_ is now vector<json>. LanguageConfig gains
model + speech_fillers/function_fillers (lists) and to_json emits engine, model,
and the fillers (speech_fillers+function_fillers when both, deprecated fillers
when one) — python parity. New contract-8 tests assert every field survives into
the rendered SWML ai block.
claude_skills discovery (#72):
Implement real SKILL.md discovery (was a stub). Walk skills_path subdirs, parse
YAML frontmatter (name/description) + body, declare one SWAIG tool per skill
({tool_prefix}{sanitized-name}); handler returns the SKILL.md body (native
execution impossible in AOT). Logic in the new claude_skills_core.hpp, shared by
the registered ClaudeSkillsSkillR (skill_registry.cpp) and the builtin — the
web_search_core dead-strip pattern, so they can't drift. Tests use a repo-local
temp skills tree with sample SKILL.md files.
Full run-ci PASS (1902 tests; SIGNATURES/DRIFT/SURFACE/FMT/LINT green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The generated REST *Params structs no longer accept bare JSON-map
brace-init; they are C++ structs constructed via designated initializers
(and DELETE/set-webhook helpers were renamed). Update every REST example
that failed the `examples` build target to construct params the way the
current generated types require, mirroring the REST wire-tests:
- rest_demo, rest_manage_resources, rest_calling_ivr_and_ai,
rest_calling_play_and_record: Calling::dial({.from,.to,.url,...})
- rest_calling_ivr_and_ai: collect .digits=json{...}/.initial_timeout,
detect .detect/.timeout
- rest_calling_play_and_record: play .play=json::array(...),
record .extras={{"record",...}}
- rest_datasphere_search: search .query_string/.count
- rest_queues_mfa_and_recordings: mfa().sms({.to,.from,.message})
- rest_bind_phone_to_swml_webhook: setSwmlWebhook(id,{.url=...})
(renamed from set_swml_webhook) + updated the routing-helper comments
- rest_manage_resources: ai_agents.delete_(id) (was .del)
DOC_AUDIT_IGNORE: add `delete_`, the reserved-word-avoidance method name
(surface records the Python `delete`), same class as the existing `del`.
examples target builds clean; full run-ci PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…they surface
Add wire/swml/state/http/wire_relay dump programs (tools/*_dump.cpp, one CMake
target each) so the porting-sdk per-surface differs can byte-compare the C++
port against the Python oracle. Mirrors the Go reference dumps. The RELAY dump
stands up an in-process IXWebSocket mock server to capture the frames the client
sends. All 5 differs PASS.
Real C++ divergences the differs surfaced, now fixed:
- HTTP: extract_sip_username was missing the tel:/plain-username branches
(only sip: with an '@' worked); added both (Python parity).
- HTTP: SWMLService routing-callback lookup was exact-match only; added the
route-prefix (endswith) fallback so a "/sip" callback fires for "/swml/sip".
- HTTP: serverless handle_lambda never dispatched SWAIG functions — it routed
the raw path through handle_request. Added the reference's lambda flow:
auth check + /swaig(-with-function) and /{function} SWAIG dispatch, else SWML.
- STATE: SWMLService.register_routing_callback stored the path verbatim; now
normalizes (strip trailing '/', add leading '/') like Python.
- STATE: AgentServer SIP-username mapping/lookup was case-sensitive; now stores
and looks up lowercased (Python parity), fixing "Bob"/"BOB"/"bob".
- RELAY: RelayClient.execute injected project_id + protocol into EVERY frame;
the reference sends params verbatim (protocol is a connect-handshake field).
- RELAY: RecordEvent decoded no url/duration/size (nested record{} fallback);
CollectEvent read result as a string and final as a plain bool — now result is
an object and final is tri-state (bool|None), matching Python.
- RELAY: Call.connect dropped ringback/tag/max_duration; added an options arg.
Also lazily resolve basic-auth credentials on first check (init_auth is now
const, called from validate_basic_auth) so a serverless auth check works without
a prior serve(). Add public read-accessors the STATE differ observes
(get_global_data, get_sip_usernames, AgentServer lookup_sip_route /
get_sip_username_mapping, SWMLService get_routing_callback_paths,
VerbHandlerRegistry get_verb_names) — documented in PORT_ADDITIONS.md as
cpp_typed_accessor; regenerated port_surface.json + port_signatures.json.
Full suite: 1902 tests pass; FMT/LINT/DRIFT/SURFACE/SPEC-PARITY/EMISSION green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add BEHAVIORAL-WIRE / -SWML / -STATE / -HTTP / -WIRE-RELAY gates that diff each surface's dump binary against the signalwire-python oracle via porting-sdk/scripts/diff_port_<surface>.py, locking the 64 cross-port Layer-D behaviors against silent regression. - TEST gate now also builds wire_dump/swml_dump/state_dump/http_dump/ wire_relay_dump (all three BUILD_MODEs) so the gates find ready binaries. - behavioral_gate() routes the --dump-cmd per BUILD_MODE exactly like emission_gate (host binary / docker exec / throwaway docker run with logs on stderr). - resolve_python_sdk() resolves the oracle checkout the same way diff_port_emission does ($PYTHON_SDK env, else ../signalwire-python adjacency) and is passed explicitly via --python-sdk so the gate never depends on `signalwire` being importable from ambient env. Full run-ci.sh: all gates PASS including the 5 new BEHAVIORAL-*. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Adds the swml_define_tool_complete_schema corpus case to tools/swml_dump.cpp:
define_tool with a COMPLETE {type,properties,required} schema renders
ai.SWAIG.functions[?lookup].parameters FLAT (pass-through), not double-wrapped.
The C++ ToolDefinition::to_swaig_json already passes a complete schema through
verbatim, so BEHAVIORAL-SWML PASSes with the case now enforced (not pending).
wire_relay_dump needs no change: C++ Call verbs transmit through the real
RelayClient (client->execute -> WS) to the in-process mock, so the dump already
observes at the client-send boundary — a build-but-never-transmit verb records
no frame. No rust-style sent_commands-Vec bug here. BEHAVIORAL-WIRE-RELAY PASSes.
Full run-ci PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Clear all six day-one gates for the C++ port (each now exits 0 without
--report-only; full run-ci.sh PASS).
doc_lang_purity (20 -> 0): rewrote every ```python fence across 16 docs
(~341 fences) into correct C++ against the real SDK API (agent_base,
swaig::FunctionResult, datamap::DataMap, contexts, skills::SkillBase,
swml::Service, web::WebService, ConfigLoader) - not fence relabels; verified
each symbol resolves in port_surface.json and audit_docs stays green. Dropped
the dead pip/from-signalwire_agents strings. FastAPI-only bits (as_router/
include_router/get_app) and the Python-only search service rewritten to the C++
reality or removed, never invented. mcp_gateway_reference.md (Python-only MCP
Gateway, not ported) excused via DOC_LANG_ALLOW.md.
doc_links (51 -> 0): repointed examples/README.md links to the real example
filenames; removed dead search_overview.md references (search is Python-only);
repointed CHECKLIST phone-binding link to the adjacent porting-sdk copy. The
C++-lambda [](const ...) / [](Call& ...) false positives and the cross-repo
../porting-sdk/phone-binding.md links excused via DOC_LINKS_ALLOW.md with
reasons.
root_hygiene (27 -> 0): deleted foreign .php-cs-fixer.* + untracked/gitignored
.sw-test-tmp/ scratch fixtures; fixed four /home/devuser & /usr/local/home
machine paths (libclang fallback -> $HOME; porting-sdk fallback -> env/adjacency
with fail-loud; test schema path -> repo-relative; CHECKLIST ref -> relative).
Load-bearing audit-contract files (PORT_*.md, CHECKLIST.md, DOC_AUDIT_IGNORE.md,
REST_COVERAGE_GAPS.md, audit_coverage*.json, port_{signatures,surface}.json)
kept at root and allowlisted via ROOT_HYGIENE_ALLOW.md (read by porting-sdk
audit scripts by relative path).
artifact_deny (18 -> 0): the published artifact is libsignalwire.a (src/*.cpp)
+ include/ headers; no cmake install/CPack rule packages the flagged root audit
files or the standalone emit_corpus/emit_skills/audit-harness build targets, so
none can enter the package. Documented in ARTIFACT_DENY_ALLOW.md.
ignore_ledger_verify (175 -> 0 hard): deleted 50 FALSE-absence rationales
(symbol actually in port_surface.json) and pruned 48 STALE entries (name absent
from all docs after the conversions); kept only genuine stdlib/nlohmann/relay-
stub/Python-decorator foreign names.
meta_consistent (1 -> 0): added HOMEPAGE_URL github.com/signalwire/signalwire-cpp
to the CMake project().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add the six shared porting-sdk day-one gates as blocking (non-report-only) run_gate entries in scripts/run-ci.sh, matching the sibling gates' style: DOC-LANG-PURITY, DOC-LINKS, ROOT-HYGIENE, IGNORE-LEDGER-VERIFY, META-CONSISTENT, ARTIFACT-DENY. All pure-Python, host-side, BUILD_MODE-blind. ARTIFACT-DENY runs in git-ls-files proxy mode: cpp has no CPack/install target and no standard package-list tool, so there is no authoritative published-file listing to feed --listing -; the proxy passes via the committed ARTIFACT_DENY_ALLOW.md. run-ci PASS with all gates green including the six new ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…s (#92) PACKAGE-SMOKE found cpp's CMakeLists.txt had ZERO install() rules, so `cmake --install` shipped nothing — a consumer got no library and no headers. Add the missing install rules for the public library + headers only (the internal audit/dump tools and examples are intentionally not installed): - install(TARGETS signalwire ...) -> <prefix>/lib - install(DIRECTORY include/signalwire) -> <prefix>/include (public headers, tree preserved so `#include <signalwire/rest/rest_client.hpp>` resolves) - install(DIRECTORY deps/ *.h/*.hpp) -> <prefix>/include so the vendored header-only deps the public headers pull in (<nlohmann/json.hpp>, httplib.h) resolve from an install that only has -I<prefix>/include Also make the library SHARED (was STATIC) so the installed artifact carries its transitive link deps (OpenSSL, ixwebsocket, zlib, Apple frameworks) as DT_NEEDED / load commands — a bare `g++ ... -lsignalwire` against a static archive can't resolve those symbols. Bake the library's absolute installed path as its runtime name (INSTALL_NAME_DIR on macOS, absolute -soname on ELF) so a consumer with no rpath/LD_LIBRARY_PATH still loads it. package_smoke.py --port cpp now green (build -> cmake --install -> consumer compile+link+run all PASS); scripts/run-ci.sh PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
public_jargon 282 -> 0: - Template fix (157 generated hits): reword the generate_rest.py banner + class/enum doc-comments so emitted headers never carry "porting-sdk" / "x-sdk-" jargon; regenerate the REST layer. - Hand reword (125 hits across 53 files): replace "Python parity:" / "(parity ...)" / "porting-sdk/webhooks.md" / "oracle" / "port-side" internal cross-references with user-facing wording (Corresponds to / matches / the reference / the webhook signature spec). route_collision: - (b) crud-dup 1 -> 0: remove the dead legacy signalwire::rest::CrudResource (2-arg PATCH-only) declaration + impl and its two construction-only orphan tests; the generated signalwire::rest::generated::CrudResource is the single public CRUD base. - (a) route-split: the list_addresses override for the singular-sub-path fabric resources is now emitted as list_addresses (snake) instead of listAddresses, so it HIDES the inherited plural-path base member -> exactly ONE live route (the spec-canonical singular path). This is the same collapse java (@OverRide) and go (embed plain CRUD base) made. Route-registry + hand/generated fabric wire-tests updated. The gate's plural-collection heuristic still flags the divergent segment; it is allowlisted in ROUTE_COLLISION_ALLOW.md with the openapi.yaml quirk citation and wire-test proof, mirroring the identical user-approved java/go entries (PENDING human approval per AGENT_RULES section 3). gen_type_degeneracy, gen_idiom, release_fresh: already clean, no change. run-ci: PASS (all gates, incl. DRIFT/SURFACE-FRESH/GEN-FRESH-TESTS/FMT/LINT). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…-07-07 class ruling) The 2 CallFlows/ConferenceRooms singular-path list_addresses entries are the same spec-canonical-single-route class the user approved for go+java on 2026-07-07 (C++ name-hiding collapses to one live route = spec singular path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Wire the Tier-5/release expansion gates as blocking (non-report-only) gates, modeled on the six Day-one gates already in run-ci.sh: GEN-TYPE-DEGENERACY generated types aren't degenerate loose aliases PUBLIC-JARGON no internal porting jargon in public doc comments ROUTE-COLLISION no route-split/crud-dup (registry-driven; allowlist honored) GEN-IDIOM generated code is not lint-excluded from the idiom linter RELEASE-FRESH publish workflow runs gates before publishing Four are pure host-side Python. ROUTE-COLLISION drives the route_registry binary via a new route_collision_gate() built + run per BUILD_MODE exactly like spec_parity_gate; its 2 fabric list_addresses route-split entries are the user-approved ROUTE_COLLISION_ALLOW.md exceptions (392bb5b). SEMVER-DIFF is intentionally NOT wired (HELD pending the version-bump decision). Also add the ROUTE_COLLISION_ALLOW.md self-reference to ROOT_HYGIENE_ALLOW.md (the allowlist file lives at repo root by the gate's convention, same as the four peer *_ALLOW.md files) — 392bb5b committed it without that entry, which ROOT-HYGIENE now flags once the gate suite runs together. run-ci: CI PASS (37 gates, clean cmake rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
docs/mcp_gateway_reference.md documented the Python-only mcp_gateway
SKILL (pip install "signalwire-agents[mcp-gateway]",
agent.add_skill("mcp_gateway"), swaig-test) which was dropped from the
C++ port. The doc is dead: it references no C++ surface.
Removed:
- docs/mcp_gateway_reference.md (the doc itself)
- README.md link to it (Skills and Extensions section)
- CHECKLIST.md coverage entry
- DOC_LANG_ALLOW.md allowlist entry (excused the now-deleted doc from
DOC-LANG-PURITY; stale once the file is gone)
KEPT: the real C++ direct-MCP feature is untouched — docs/mcp_integration.md
(add_mcp_server / MCP Client+Server), src/skills/builtin/mcp_gateway.cpp,
examples/mcp_agent.cpp, examples/mcp_gateway_demo.cpp, and the MCP tests.
run-ci PASS (37/37 gates, incl. DOC-LANG-PURITY + DOC-LINKS clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…) (#104)" This reverts commit fe20fbf.
The doc was copied verbatim from python and documented the python-only
gateway SERVER internals (gateway_service.py, `python3 gateway_service.py`,
`pip install ...[mcp-gateway]`, server config.json deep-dive, Docker) that the
C++ SDK cannot run. Rewrite it to document C++'s real built-in `mcp_gateway`
CLIENT skill (src/skills/builtin/mcp_gateway.cpp).
- Show the `add_skill("mcp_gateway", {...})` idiom with the params the skill's
setup() actually reads: gateway_url (required), tool_prefix (default mcp_),
and services (array of {name}). Dropped the invented auth/session/retry/ssl
params — the C++ skill does not read them.
- Keep the language-neutral wire contract (Protocol Flow, Message Envelope).
- Replace python-CLI examples with the C++ bin/swaig-test CLI.
- Add a short "Running a gateway server" section pointing at the separate,
operator-run gateway component instead of implying the port ships a server.
- Remove the now-stale DOC_LANG_ALLOW.md entry (doc is pure C++).
647 -> 176 lines. run-ci PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add a --parse-only flag (alias --dry-run) to the cpp swaig-test bash CLI mirroring the python reference contract. It is detected and stripped from argv up front so it is position-independent — honored whether it precedes or trails an --exec <tool> (which would otherwise consume the next token as the function name). After the normal arg-validation loop it short-circuits: prints exactly "parse OK" and exits 0 on valid args, WITHOUT loading/introspecting any agent or example binary and WITHOUT any network call. Invalid args exit 2 (via die() in parse-only mode) with no "parse OK". Fast (<100ms even against an unreachable --url). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…xtures Anchor the three README quickstart code blocks (AI agent, RELAY client, REST client) to gate-compiled fixtures under examples/ via <!-- include: --> markers, so the doc code is byte-identical to real, compiling code and can never rot. Fix two REST typed-API doc bugs surfaced by byte-matching the block to a compiling fixture: the REST API is now typed, so calling().dial() and datasphere().documents.search() take typed params structs (designated initializers), not JSON maps. The README showed the old map form which no longer compiles. Fixtures build via the CMake examples target (SNIPPET-COMPILE path); gate readme_include.py reports 3 sites clean; run-ci PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
README-INCLUDE now runs as a blocking gate: every doc code block anchored by an <!-- include: --> marker must stay byte-identical to its gate-compiled fixture region. Doc rot for converted quickstart blocks is now impossible — the doc code IS the compiled fixture. Port README already converted + gate clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…cation, and real typed-API doc-rot fixes
Every fenced cpp block now compiles against the real SDK headers
(snippet_compile.py --port cpp --report-only: 172 failures -> 0; 521
compiled, 43 no-compile-marked).
Fixes span three classes:
- Page-setup preambles + fully-qualified namespaces (signalwire::agent::,
::swaig::, ::datamap::, ::skills::, ::swml::, ::rest::, ::web::) and
agent./service. receiver prefixes for member-call fragments.
- Real typed-API doc rot (matches the go/rust drift):
- REST calling: methods moved to typed *Params structs constructed with
C++17 designated initializers; docs still showed the old json-map API
(dial({{"from",...}}) -> dial({.from=...})). Fixed field-order and
arity bugs (ai_stop/denoise now take the 2nd Params arg).
- REST namespaces: generated per-op methods are camelCase (listCampaigns,
createToken, listEvents, setSwmlWebhook, ...), delete is delete_ not del;
many resources take typed *Params not json; corrected wrong field names
(addresses country/first_name..., verified_callers verification_code,
imported_numbers number_type, sip_profile domain_identifier).
- phone-binding: replaced fictional Options types with the real
Set*Params designated-init calls.
- swml service: add_verb is 3-arg (section, verb, params); Logger lives in
signalwire:: (not ::logging::).
- AgentBase::serve() takes no args (port comes from the constructor);
get_execution_mode lives in signalwire::core::logging_config.
- set_functions takes variant<string,vector<string>>: multi-element brace
lists need std::vector<std::string>{...}.
- umbrella signalwire.hpp omits agent/agent_base.hpp (added explicit
include in the affected block).
- no-compile markers for genuinely-illustrative blocks (method-signature
listings, cross-block references to the reader's own classes/helpers,
before/after migration snippets).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…er.hpp (#119)
The umbrella header pulled in contexts/datamap/rest/skills/swaig/swml but NOT the
two most central classes — agent/agent_base.hpp and server/agent_server.hpp — so a
user who `#include <signalwire/signalwire.hpp>` and nothing else could not construct
an AgentBase or AgentServer (undeclared). Added both in sorted position.
Verified: a TU including only the umbrella now compiles `AgentBase agent("t","/")`
and `AgentServer server;` (no circular-include). Found by the #90 cpp doc burn +
flagged by the group-A sub-agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The #90 cpp doc burn removed the last reference to `schedule_follow_up`, leaving its DOC_AUDIT_IGNORE.md entry orphaned — IGNORE-LEDGER-VERIFY hard-fails on a stale entry (the anti-laundering rule: an ignore must not excuse a symbol that no longer appears in any doc/example). Confirmed absent from all cpp docs/examples; pruned. Gate green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…rators Load porting-sdk/rest-apis/x-sdk-overlay.yaml in the shared REST + SWML-verb generator and apply its SDK-surface policy at the method-less DTO emission site (emit_methodless_struct, used by both generate_rest.py and generate_swml_verbs.py): - hidden fields are dropped from the generated surface entirely (still on the wire via `extras`); - deprecated fields are emitted with a C++ `[[deprecated]]` attribute. The overlay `scope` is matched against the SPEC schema name (the $defs / components.schemas key `AIParams`) threaded through as `schema_name`, NOT the emitted C++ struct name. Effect on the 3 AIParams DTO headers (swml_verbs + calling + fabric REST types): the 5 server-internal params (audible_debug, audible_latency, verbose_logs, enable_accounting, cache_mode) are gone; languages_enabled is kept and marked `[[deprecated]]`. enumerate_signatures.py's POD-struct field regex (`_GEN_FIELD_RE`) now tolerates a leading `[[attr]]` so a deprecated member is still recorded on the signature surface (the surface enumerator already stripped attributes). Without this, `[[deprecated]] languages_enabled` was silently dropped and drifted against the oracle, which keeps it. Regenerated port_signatures.json. sw-verify cpp: SURFACE-DIFF / DRIFT / GEN-FRESH (+SWML/RELAY/SWAIG/TESTS) / NO-LAUNDER all PASS; 1900/1900 tests pass; FMT no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…elpers / migration name)
DOC-AUDIT flagged 27 unresolved symbols in docs/examples, none a public SDK-surface method:
C++ stdlib (atoi/atof/getline/localtime/tolower/toupper/transform/time/signal/make_pair/…),
std::chrono literals (hours/seconds/milliseconds), nlohmann::json calls (object/array),
internal free-function helpers in namespace signalwire::{skills,common,rest}
(http_get/http_post/ensure_builtin_skills_registered/to_wire_string/get_env — none has a
python-oracle public equivalent, verified against python_surface.json), and SignalWireClient
(the OLD 2.x class name shown verbatim in MIGRATION-2.0.md's rename table).
All added to DOC_AUDIT_IGNORE.md §11 with per-entry rationale. Pre-existing on the branch,
surfaced when CI re-ran; unrelated to the AI-params overlay change. IGNORE-LEDGER-VERIFY: no
hard findings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adopts the spec-driven generated REST surface for the C++ SDK, replacing the previous hand-written/loose REST layer. The REST client, its typed inputs, the read-side payload types, and the full wire-test suite are now generated mechanically from the canonical specs (
porting-sdk/rest-apis/*/openapi.yaml+x-sdk-*markup) and kept in lock-step by--checkfreshness gates — matching the Python reference surface in idiomatic C++.What the branch delivers
set_methods, spec-composed paths, and the namespaced container tree (client.fabric.ai_agents.list(...)etc.). Hand-written bases; everything else generated.extrasescape door for forward-compatible wire fields. Twilio-compat (LAML) removed. Generated POD-struct payload fields projected as property getters.End state
All 20 CI gates pass against
porting-sdkmain — TEST, SIGNATURES, DRIFT (0 signature drift vs the Python oracle), SURFACE-DIFF (0), SURFACE-FRESH, GEN-FRESH (every generated file reproduces from its spec), REST-COVERAGE, SPEC-PARITY, EMISSION, SKILL-CONTRACT, SWAIG-COVERAGE, DOC-AUDIT, FMT, and LINT. The generated surface matches the Python reference; unknown/idiom divergences are recorded inPORT_OMISSIONS.md/PORT_ADDITIONS.mdwith rationales (no silenced gaps).🤖 Generated with Claude Code