Skip to content

feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the C++ SDK#25

Merged
mjerris merged 55 commits into
mainfrom
feat/rest-generated
Jul 8, 2026
Merged

feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the C++ SDK#25
mjerris merged 55 commits into
mainfrom
feat/rest-generated

Conversation

@mjerris

@mjerris mjerris commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

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 --check freshness gates — matching the Python reference surface in idiomatic C++.

What the branch delivers

  • Generated REST client. the generated per-namespace resource headers + the client tree, emitted from the specs — per-resource CRUD + operation + command-dispatch + set_methods, spec-composed paths, and the namespaced container tree (client.fabric.ai_agents.list(...) etc.). Hand-written bases; everything else generated.
  • Typed write surface. create/update/operation methods take the closed, spec-typed parameters plus an extras escape door for forward-compatible wire fields. Twilio-compat (LAML) removed. Generated POD-struct payload fields projected as property getters.
  • Read-side typed payloads. Generated types for the SWAIG request / post-prompt / response-action shapes and the SWML verb-config surface.
  • Generated wire-test suite. The full success+error REST wire tests, generated from the route-registry × spec join and freshness-gated — every implemented route covered.
  • Strict source + tests. clang-tidy curated set (zero findings); formatter clean (clang-format).

End state

All 20 CI gates pass against porting-sdk main — 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 in PORT_OMISSIONS.md / PORT_ADDITIONS.md with rationales (no silenced gaps).

🤖 Generated with Claude Code

@mjerris mjerris marked this pull request as ready for review July 4, 2026 03:16
mjerris and others added 9 commits July 3, 2026 23:44
…-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
@mjerris mjerris force-pushed the feat/rest-generated branch from a6eba0b to 6e280ee Compare July 4, 2026 03:46
mjerris and others added 12 commits July 4, 2026 10:38
… -> 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>
@mjerris mjerris changed the title [WIP] feat(rest): full A–I rollout + CI perf (DO NOT MERGE — review) feat(rest): full A–I rollout + CI perf Jul 5, 2026
@mjerris mjerris changed the title feat(rest): full A–I rollout + CI perf feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the C++ SDK Jul 5, 2026
mjerris and others added 5 commits July 5, 2026 12:40
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
mjerris and others added 27 commits July 5, 2026 20:52
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
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
@mjerris mjerris merged commit 2bc8a5f into main Jul 8, 2026
3 checks passed
@mjerris mjerris deleted the feat/rest-generated branch July 8, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant