Skip to content

chore(surface): fold onto agentbase-family + B1 composition attrs + canonical SWML verbs - #53

Merged
mjerris merged 22 commits into
mainfrom
chore/allowlist-fold-cpp
Jul 27, 2026
Merged

chore(surface): fold onto agentbase-family + B1 composition attrs + canonical SWML verbs#53
mjerris merged 22 commits into
mainfrom
chore/allowlist-fold-cpp

Conversation

@mjerris

@mjerris mjerris commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Brings the C++ port green against porting-sdk@fix/agentbase-mixin-flatten-fold (the shared A/B1/G/D1 idiom folds) by fixing idiom at EMISSION (the enumerator) and re-keying the now-orphaned allow-list entries to the folded tokens.

Drift: 194 surface omissions + 28 additions + 8 bad-reason omissions + 22 signature drifts → 0

Pre-fold the port was green; the fold branch's oracle added 179 B1 composition/struct-field members the C++ regex header walker never surfaced (it emits methods, not public data members), and the A/G folds re-keyed AgentBase/mixin/verb symbols — orphaning the old-key allow-list entries.

Emission (scripts/enumerate_surface.py)

  • _project_gen_payload_members — project generated read-side payload struct fields (swml_verbs_generated, post_prompt_generated, swaig_request_generated) as surface members, intersected with the oracle's B1 attributes (mirrors the signature side's _project_gen_payload_getters). Retires ~174.
  • _project_client_tree_members — project namespace-container resource accessors from the real <Type> <member>; data members (the walker only caught ctor-init-list entries and missed the first). Retires 12.
  • Surface RequestOptions.abort_signal, PromptObjectModel.sections, Section.subsections (public data members) + relay.call.Action.result.
  • Normalise the SWML keyword-escape verbs goto_section/return_section/switch_section → canonical goto/return/switch in _METHOD_RENAMES so the class-G fold retires them.

Allow-lists

  • PORT_ADDITIONS: removed 28 dead agent_base.AgentBase.* entries; added 28 re-keyed agentbase-family.* (each verified to have no reference twin — the §4c residual; WHY tightened from the now-false python_mixin_collapsed).
  • PORT_OMISSIONS: re-keyed ToolMixin.tool/WebMixin.get_app/AgentBase.skill_manageragentbase-family.*; deleted 2 now-emitted pom entries; corrected 9 B1-@Property reasons (see below).
  • PORT_SIGNATURE_OMISSIONS: +22 raw-key entries restoring the signature-side cross-excuse the fold moved (the signature diff matches raw keys, not folded).
  • Added the ALLOWLIST_DISCIPLINE §1 header to all three Tier-A files.

⚠️ Needs sign-off — 9 B1 @property port-gaps

logger×4 / AgentServer.agents / SkillManager.loaded_skills / SWMLService.security / WebService.security / AgentBase.skill_manager are public class-typed Python @property attrs — NOT language-impossible. C++ currently reaches the same state via a getter-METHOD of a different shape (get_agents/list_loaded_skills) or keeps it private (module-level logger, private composition). I documented each as a port-gap (approved: cpp-… — …pending API sign-off; NOT language-impossible) rather than a bare impossible:. Owner decision: ratify as approved-with-reason, or convert to fix-port (add matching C++ accessors).

Scope

Zero *Resource/D2-namespace/CRUD-emission changes (deferred to the spec-driven REST pass). The only REST-adjacent change is the class-B1 namespace-accessor projection.

Verification

Full run-ci.sh green: TEST, SURFACE (8/8), GEN (5/5), BEHAVIORAL, DOC-TRUTH, LEDGER, PACKAGE, NO-CHEAT, FMT all PASS (nightly-tier gates SKIP per-PR). Enumerate is deterministic and adjacency==env resolves the fold-branch oracle (CI-safe).

Coordinated-With: porting-sdk@fix/agentbase-mixin-flatten-fold

🤖 Generated with Claude Code

https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t

…anonical SWML verbs

Bring the C++ port green against porting-sdk@fix/agentbase-mixin-flatten-fold:

Emission (scripts/enumerate_surface.py):
- Project generated read-side payload struct fields (swml_verbs_generated,
  post_prompt_generated, swaig_request_generated) as surface members, intersected
  with the reference oracle's B1 composition attributes — mirrors the signature
  side's _project_gen_payload_getters. Retires ~174 phantom omissions.
- Project the generated namespace-container resource accessors
  (_client_tree_generated) from the real <Type> <member>; data members
  (the regex walker only caught ctor-init-list entries and missed the first).
- Surface RequestOptions.abort_signal, PromptObjectModel.sections,
  Section.subsections, and relay.call.Action.result (public members the oracle
  now records via B1; the walker skips data members).
- Normalise the SWML keyword-escape verbs goto_section/return_section/
  switch_section to canonical goto/return/switch so the class-G fold retires
  them (idiom fixed at emission).

Allow-lists:
- Re-key the 28 dead AgentBase.* additions and the ToolMixin.tool/WebMixin.get_app
  omissions to the folded agentbase-family.<m> token (ALLOWLIST_DISCIPLINE §4c);
  tighten each WHY (composition-delegate / port-only helper — no reference twin).
- Correct the 9 B1 @Property omissions (logger/agents/security/loaded_skills/
  skill_manager): NOT language-impossible — documented as port-gaps (getter-method
  form vs @Property, or private composition) pending accessor API sign-off.
- Add raw-key PORT_SIGNATURE_OMISSIONS entries restoring the signature-side
  cross-excuse the fold moved (the signature diff matches raw keys, not folded).
- Add the ALLOWLIST_DISCIPLINE §1 header to all three Tier-A files.

REST-CRUD entries left untouched (deferred to the spec-driven REST pass).

Coordinated-With: porting-sdk@fix/agentbase-mixin-flatten-fold

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
@mjerris mjerris changed the title chore(allowlist): fold-driven allow-list cleanup + §1 header (cpp) chore(surface): fold onto agentbase-family + B1 composition attrs + canonical SWML verbs Jul 24, 2026
@mjerris mjerris added the coordinated-pass Coordinated cross-port fold pass label Jul 24, 2026
mjerris and others added 5 commits July 24, 2026 13:20
…ntical)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-level loggers (no per-instance field exists), approved: for the 5 accessor-expressible B1 attrs

The logger attrs are a genuine structural absence (C++ logging is module-level;
no per-instance logger field exists) -> impossible: is the honest token. The
5 accessor-expressible attrs (agents/loaded_skills/security x2/skill_manager)
stay approved: cpp-<specific> — pending API sign-off (getter-method form / private
composition), NOT language-impossible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
The surface enumerator now consults python_surface.json for the composition-attribute
projection (this pass's B1 emission). surface-audit.yml's standalone --check step didn't
export PORTING_SDK, so the enumerator's <repo>/../porting-sdk adjacency fallback missed
CI's in-repo ./porting-sdk checkout, silently degraded the regen, and failed as a phantom
'stale relative to headers'. Reproduced locally (no PORTING_SDK + no ../porting-sdk sibling
= DRIFT; PORTING_SDK=./porting-sdk = clean). NOT a Python 3.13/3.14 issue — the enumerator
is version-stable; it was the missing oracle path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
…tions, delete dead omissions

Tighten the green allow-list-fold PR now that the spec-driven REST fold, the
AgentBase-family (A) fold, and the SWML-verb (G) fold are in place, and the new
SURFACE-DIFF dead-addition check is landing.

PORT_ADDITIONS 641 -> 390:
- REST 214 -> 68: C++ emits its generated resources under the canonical
  <ns>_resources_generated.<Class> names, so their CRUD folds via the reference
  crud_bases map — no port crud_bases map needed (the php/ts path, not the
  rust/cpp-rename path). 146 stale pre-refactor hand-scheme entries were already
  dead; kept 68 genuine port-only members (D3 HTTP plumbing, error observability,
  pagination helper, typed-namespace dispatch, generated ResourceTree) with crisp WHY.
- 107 dead A-fold / G-fold / folded-type additions removed: 69 pure-deleted;
  38 that are dead for SURFACE-DIFF but LIVE for the signature DRIFT gate
  (36 SWMLService verbs + AgentBase.handle_request + CrudResource/ReadResource
  __init__) relocated to PORT_SIGNATURE_OMISSIONS with their WHY.

PORT_OMISSIONS 147 -> 99: deleted 48 dead omissions whose symbol is no longer in
the reference oracle (RAG/search backends google_maps/web_search, historical
skill_improved/skill_original variants, pre-refactor REST hand names, AgentServer.app
surface form).

PORT_SIGNATURE_OMISSIONS 299 -> 331: +38 relocated dual-gate entries + AgentServer.app
(signature missing-port; its surface omission was correctly deleted as dead-for-surface);
-7 dead phone_numbers.PhoneNumbersResource.set_* under the old naming (now emitted
under relay_rest_resources_generated.PhoneNumbers).

Accessor-omission re-audit (rename rule): zero renames — none of the 24 composition
attrs expose the member under a different name; AgentServer.agents / SkillManager
.loaded_skills keep as omissions (renaming would orphan the name-matched get_agents /
list_loaded_skills method); module-level loggers stay impossible: (no per-instance field).

SURFACE + DRIFT green (all 8 rules PASS); JSONs unchanged (SURFACE-FRESH clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
…lass fields onto oracle

The oracle (porting-sdk 7693802) now records @DataClass public fields for the
94 signalwire.relay.event classes, the 3 AI-Chat result DTOs
(ChatResponse/ChatLog/ConversationInfo) and RequestOptions. The C++ port carries
these as public struct data-member fields that the regex header walker (surface)
and libclang (signatures) emit no method for, so every oracle field/getter read
as missing-port DRIFT.

Fold the field-idiom onto the oracle in both enumerators:
- enumerate_surface.py: _struct_public_fields parses named struct/class bodies
  (inheriting form incl. ": public RelayEvent"), _emit_oracle_gated_fields unions
  each struct's public data-member fields into the module intersected with the
  oracle's per-class member set. Wired for relay.event (typed_events.hpp),
  ai_chat.client DTOs, and rest._request_options.
- enumerate_signatures.py: _project_named_struct_getters projects the same fields
  as zero-arg property getters, gated on the oracle's per-class getter set. The
  AI-Chat DTO getters are emitted in _project_ai_chat_signatures.

The intersection with the oracle is the guard against inventing surface. Drop the
47 stale signalwire.livewire "approved:" omissions — the oracle no longer records
livewire at all (LiveKit ships no C++ agents SDK), so the entries reference
nonexistent reference symbols.

SURFACE-DIFF exit 0 (2538 symbols), SIGNATURES-DIFF exit 0 (1415 ref symbols).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
@mjerris

mjerris commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Coordinated set root: signalwire/porting-sdk#120 (branch fix/agentbase-mixin-flatten-fold, now open for review).

That PR carries the shared oracle/diff folds this branch is pinned to (PORTING_SDK_REF is set on this repo) plus the allow-list doctrine governing the burn-down — ALLOWLIST_DISCIPLINE.md §0/§0a/§0b/§7/§8/§9. Short version: idiom is standardized by the code that does the emission and is never an addition or omission; an AI may not create either — fold, implement, delete, or propose-and-stop.

Merges as a set, porting-sdk first. No merge before approval.

Owner ruling 2026-07-24: logging is a MODULE-LEVEL capability ports may
reach however their language does; the per-instance logger/log attribute
is not contract. porting-sdk 7f2a67f dropped it from python_surface.json,
which made these 4 entries DEAD and failed the surface gate:

  signalwire.agent_server.AgentServer.logger
  signalwire.core.skill_base.SkillBase.logger
  signalwire.core.skill_manager.SkillManager.logger
  signalwire.skills.registry.SkillRegistry.logger

Also drops the now-stale `self.logger` example from the section prose.

SURFACE diff: 4 dead -> 0, exit 0 (2538 symbols; 39 excused omissions,
418 excused additions).

KNOWN BLOCKER (not fixable from this repo): 7f2a67f regenerated
python_surface.json but NOT python_signatures.json, which still records
`logger` on all 4 classes. The signature gate reads PORT_OMISSIONS.md via
--surface-omissions, so with these lines gone it reports 4 missing-port
drifts. SURFACE and SIGNATURES are therefore mutually exclusive until the
signature oracle is regenerated for the same ruling. This is fleet-wide,
not cpp-specific (signalwire-go reproduces it identically on
chore/allowlist-fold-go). Committing the end state that matches the owner
ruling; the oracle regen is the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1fEcuaivoN1poPGS75jAy
@mjerris

mjerris commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Re-running CI against porting-sdk acfae9e (the coordinated pin now carries the signature-oracle lockstep fix 37dcb2a, which several of these reds predate).

mjerris and others added 15 commits July 24, 2026 23:04
…ure defects

Adopts the construction contract (porting-sdk ALLOWLIST_DISCIPLINE.md §10) for
cpp, after java proved it. 146 classes / 452 named params.

WHY

cpp carried 62 `__init__` signature-omissions — 18.7% of its ledger. Each excused
a WHOLE constructor from comparison, because `compare_param` matches by POSITION
and ignores names, which is meaningless against a 22-kwarg reference ctor. One
line hid every parameter at once: the port could silently drop `record_stereo` or
`signing_key` forever and no gate would notice.

WHAT IT EMITS

A `construction` node: a NAME-KEYED, unordered set of configurables per class.
cpp reaches construction three ways and all three are the same contract, merged
in precedence order (a real ctor param's `required` wins over the implicit
optionality of the other two):

  1. CONSTRUCTOR PARAMS — the ordinary case.
  2. AGGREGATE/CONFIG-STRUCT PUBLIC FIELDS — `RelayConfig`, `RequestOptions`, the
     24 typed relay-event payload structs. C++ aggregate-initializes these BY
     FIELD NAME, so the public data members ARE the named set. Collected from the
     AST (`FIELD_DECL`), not a text scan, so inline method-body locals can never
     leak in. Fields-only PODs (`RelayConfig` has no methods, so the signature
     inventory never emits it as a class) are captured separately and UNFOLDED
     where they appear as a ctor param's type — `RelayClient(config)` becomes its
     8 real configurables instead of one opaque carrier. The unfold is skipped
     when the reference ITSELF carries that param name (`RestClient`'s
     `request_options`), where the carrier is the contract.
  3. ONE-ARG `set_x` SETTERS — the `Service()`-then-`set_host(…)` idiom, the C++
     analogue of a Java builder setter.

Deliberately NOT a source: bare zero-arg methods. A zero-arg `x()` is as likely a
BEHAVIOUR (`stop()`, `connect()`, `render_swml()`) as a field read; admitting them
invented ~1500 params. Likewise `class:`-typed fields are excluded — in the
generated REST containers those are sub-resource NAVIGATION HANDLES built in the
ctor's member-init list from the single `http` arg, not configurables (that
member-init list is invisible under PARSE_SKIP_FUNCTION_BODIES, which the
umbrella parse needs for its 3-10x speedup, so the field TYPE is the
discriminator). Both exclusions cut emitter noise from ~2018 params to 452.

ADAPTER CANONICALIZATION (renames, never omissions — §7 / RULES §2)

  * `RestClient(space, project_id)` -> reference `host`, `project`.
  * The REST transport arg: C++ `client`, reference `http`, across ~49 generated
    resource classes. Expressed as a RULE keyed on the reference's own param set
    rather than 49 hand rows (a hand list rots as resources are added), and gated
    so `relay.call.Call` — where BOTH spell it `client` — is untouched.

3 REAL SIGNATURE DEFECTS FIXED (not construction-only)

A COPY constructor is 1-arg, so under the default fewest-param dedup it BEAT the
real ctor and erased its params from the inventory. `PromptManager` and
`ToolRegistry` recorded `__init__(other: AgentBase)`; `WebService` recorded
`__init__(_: WebService)` — a self-referential param literally named `_`. Copy/
move ctors are now filtered at the walker. Separately, `__init__` was being MOVED
off `AgentBase` by the PromptManager/ToolRegistry mixin projection (it must be
COPIED — AgentBase has its own real `AgentBase(name, route, host, port)`), which
had erased the construction contract for the port's widest class. Net: +1 symbol
(`AgentBase.__init__`), 3 corrected, 0 lost.

WHAT IT REVEALED — 270 findings, previously invisible

  construction-missing-param   146
  construction-extra-param      86
  construction-required-flip    16
  construction-missing-class    16
  construction-type-mismatch     6

On AgentBase alone, 15 reference configurables a cpp developer cannot set:
agent_id, auto_answer, basic_auth, check_for_input_override, config_file,
default_webhook_url, enable_post_prompt_override, native_functions, record_call,
record_format, record_stereo, schema_path, schema_validation, suppress_logs,
token_expiry_secs.

Includes a probable WIRE BUG for owner review: the reference declares
CallingErrorEvent.code and ReferEvent.sip_{refer,notify}_response_code as `str`
(its test asserts the string "202"), while cpp parses them as `long` via
`p.value<long>(...)` — a JSON string payload would not extract.

These are REPORTED, not ledgered: per §0a an agent may not create an omission or
an addition. They are implement-work for the owner.

VERIFIED (real gates, sibling-adjacent checkout)
  diff_port_signatures.py   exit 0 — 1411 reference symbols, 1874 port symbols
  diff_port_surface.py      exit 0 — 2538 symbols, 39/418 excused
  suites/surface.py         exit 0 — all 8 rules PASS (incl. SURFACE-FRESH)
Enumerator output is deterministic and surface-neutral (port_surface.json
byte-identical apart from its embedded commit-hash stamp). No cmake build needed;
this changes the enumerator only, no C++ source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1fEcuaivoN1poPGS75jAy
…construction

Two confirmed defects.

WIRE DEFECT — typed RELAY event field types (cpp was the only port with it).
ReferEvent.sip_refer_response_code / .sip_notify_response_code and
CallingErrorEvent.code were declared `long` and parsed with `p.value<long>(...)`.
The reference declares all three as `str` and its own test asserts the STRING
value (signalwire-python tests/unit/relay/test_event.py:291,298 —
"sip_refer_response_code": "202" / == "202"). A JSON string does not extract
into a `long`, so the field silently came back 0. Every other port uses a
string type. Now `std::string` + `value<std::string>`.

Found in the same pass: TranscribeEvent.duration was `long` where the
reference declares `float` — a fractional wire duration truncated. Now
`double`.

AGENT CONSTRUCTION — AgentBase/Service accepted 4 of the reference's 22 / 8
constructor parameters, so 19 configurables were unreachable even though the
underlying capability already existed in the tree. Both constructors now take
the reference's full parameter list and FORWARD each one to the same
collaborator the reference forwards it to:
  * schema_path + schema_validation -> SchemaUtils
  * config_file                     -> the `service` config section supplying
                                       name/route/host/port defaults
                                       (constructor arguments win)
  * basic_auth                      -> the auth credentials
  * token_expiry_secs               -> SessionManager (previously a hardcoded
                                       3600 that no caller could change)
  * port                            -> optional, so "not supplied" still falls
                                       back to config file then PORT env

Three of these now have real wire effect that was previously absent:
auto_answer gates the PHASE-2 `answer` verb, record_call emits the PHASE-3
`record_call` verb with format/stereo, and default_webhook_url emits
ai.SWAIG.defaults.web_hook_url.

Accessor visibility follows the reference's own: `agent_id` and
SessionManager's `token_expiry_secs` are public attributes in the reference
but are exposed here as protected, because the signature oracle does not
enumerate __init__ attributes (the class-B2 blind spot) and a public accessor
would therefore read as port-invented surface. No omission or addition entries
were added.

Tests: 2014/2014 pass, including 13 new construction-forwarding tests and 4
new typed-event tests. SURFACE suite 8/8 PASS; diff_port_surface.py and
diff_port_signatures.py both exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1fEcuaivoN1poPGS75jAy
The oracle's new class-B2 rule records a public __init__ attribute that is also
a constructor param of the same class: caller-supplied configuration the caller
must be able to read back. Measured against it, cpp had 74 missing symbols.
All 74 are closed by folding or implementing; no ledger entry was added.

UNDOES A CAPABILITY LOSS THE OLD CHECK FORCED

AgentBase::agent_id() and SessionManager::token_expiry_secs() were *protected*
(with `friend class AgentBase`) purely to keep SURFACE-DIFF green, so a C++
caller could not read what every Python caller can. Both are public now, and
the friend declaration is gone.

FOLDED AT THE ENUMERATORS (idiom, ALLOWLIST_DISCIPLINE §0)

Three general folds replace what would otherwise have been dozens of entries:

  * Public DATA-MEMBER fields are surface. The surface walker emitted only
    methods, so every public field read as missing-port drift even though the
    member was right there (pom::Section, relay::Message). Both enumerators now
    emit fields, intersected with the oracle's member set for the SAME class so
    a port-internal field can never become invented surface.
  * `set_<x>` folds onto `<x>` where the oracle records `<x>` on the same class.
    The reference spells caller-supplied config as one public attribute (read
    AND write); C++ splits it into accessor + fluent setter. A `set_<x>` the
    oracle records verbatim (FunctionResult.set_response) is left alone.
  * AI-Chat / relay-Action / PromptManager / ToolRegistry projections updated:
    url, code, message (C++ server_message()), control_id and call are
    reference members now, not dropped getters.

SignalWireRestError.status renamed to status_code — the artifact writer already
emitted the key as status_code, so the short name was a gratuitous divergence.
Same for relay::Message from/to -> from_number/to_number, which are the wire
keys the fields are parsed from.

IMPLEMENTED (TDD-bidirectional, 20 new tests, real behavior)

Read accessors for construction params the reference keeps public:
WebService (5), GatherQuestion (6), ConfigLoader, SchemaUtils, AuthHandler,
DataMap, FunctionResult (2), SWMLService.host, and prefab config on
SurveyAgent / FAQBotAgent / ConciergeAgent (several of which the port had been
rendering into a prompt string without keeping).

Genuinely-absent capability the port lacked:

  * RELAY JWT auth. RelayConfig.jwt_token + SIGNALWIRE_JWT_TOKEN; the connect
    frame authenticates with {"jwt_token": ...} alone, and JWT is the
    alternative to the project/token pair rather than an addition to it. The
    port had no JWT path at all, so a caller holding only a JWT could not
    connect. Wire-asserted against the mock.
  * Message.context / Message.segments — the mock has always pushed both on
    messaging.receive and the port dropped them, so an inbound handler could not
    tell which context a message arrived on nor how many segments it used.
  * Call.project_id / context / device / segment_id, populated from the inbound
    event exactly as _handle_inbound_call does (context prefers the
    connect-issued protocol; project_id falls back to the client's project).
  * Action::call() — the back-reference to the Call, resolved through the
    client's registry (which owns the Call) so a long-lived Action cannot
    dangle.
  * AgentServer log_level, stored lowercased and applied to the logger in run()
    (the reference forwards it to uvicorn there, and constructing a server must
    not mutate global logging state).
  * SkillBase.agent / .params and SkillManager.agent. The reference constructs a
    skill as SkillClass(agent, params); C++ default-constructs through the
    registry factory, so the loader injects both via a `bind` that is private
    and friended to SkillManager — a skill still cannot re-parent itself.

BEHAVIOR CHANGE — SessionManager HMAC key

The reference signs with self.secret_key.encode(): the KEY is the secret_key
STRING's bytes. The port keyed the HMAC with 32 raw bytes. secret_key is now a
string (a caller's value, else a secrets.token_hex(32)-shaped generated one) and
the byte-vector constructor hex-encodes into it, so both spellings sign
identically and identically to the reference. Tokens are self-contained
per-manager, so no previously-issued token crosses a trust boundary. Two
contract-7 tests were signing with the raw bytes; they now read the manager's
own secret_key(), which is what makes them the python-format interop check they
claim to be.

LEDGER

13 dead PORT_ADDITIONS deleted (setters and error-envelope accessors the oracle
now records). One entry survives: SignalWireRestError.request_id, with a
corrected rationale — the reference DOES set self.request_id publicly, but B2
only records an __init__ attribute that is also a ctor param, and request_id is
DERIVED from the headers param. Proposed for owner ruling: extend B2 to a
public __init__ attribute derived from a ctor param.

Verification: diff_port_surface exit 0, diff_port_signatures exit 0,
2034/2034 tests, run-format --check 0, run-lint 0, audit_no_cheat_tests clean,
generate_rest_tests regeneration byte-clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1fEcuaivoN1poPGS75jAy
The B2 re-drift (452801a) took cpp to zero missing symbols and both fast diffs
to exit 0, but it moved things the fast diffs do not look at. Four rules went
red: DOC-AUDIT, DOC-ENV, ACCESSOR-TRUTH (doc_truth) and BEHAVIORAL-WIRE
(behavioral). They have three distinct causes, not one.

DOC-AUDIT — MECHANISM FIX (the reusable one)

B2 folded `set_<x>` onto `<x>` at the enumerator, so the emitted surface
carries the reference's spelling. That is correct for the parity diff and
wrong for the doc gates: `svc.set_route("/demo")` is correct, compiling C++
naming a method that genuinely exists, but `set_route` is no longer in
port_surface.json, so DOC-AUDIT read 8 names as phantoms across ~23 sites in
docs/ and examples/ (set_route, set_port, set_host, set_faqs, set_amenities,
set_venue_name, set_signing_key, set_suggest_related).

The doc gates already know how to resolve this: audit_docs.py takes
--native-names and unions that sidecar with the folded surface, and
suites/_doc_audit.py passes port_surface_native.json for ANY port that ships
one. cpp shipped no sidecar — only dotnet and java did. So enumerate_surface.py
now emits one: build_native_names() walks the same headers and records the
declared names VERBATIM, before _fold_setters and before _gate_field_members
drop anything. Written beside port_surface.json, covered by --check with the
same staleness diagnostic, in dotnet's flat {"native_names": [...]} shape.

The parity diff keeps comparing against the reference's spelling; the doc gates
now see what a caller can actually type. Nothing here gates on the oracle —
these are the port's own declarations, used only to RESOLVE doc references,
never to claim surface.

This is the fold's general doc-drift, so any port that folds accessors will hit
it and can close it the same way — the sidecar consumer is already shared.

ACCESSOR-TRUTH — the doc was genuinely wrong

Not the same cause, despite arriving in the same pass. B2 renamed
SignalWireRestError.status to status_code, and rest/docs/{client-reference,
getting-started}.md still showed e.status(). That method does not exist under
any spelling — the docs promised a call that no longer compiles, so they are
corrected rather than resolved. The stale prose in http_client.hpp:77 is fixed
too.

A sidecar entry would NOT have been the right answer here even though `status`
happens to appear in it (unrelated ai_chat / typed_events classes declare a
`status` field). The sidecar is name-keyed, not class-scoped, so leaning on it
would have let a genuinely broken doc resolve by coincidence.

DOC-ENV — new code, undocumented

B2 added the RELAY JWT auth path, which reads SIGNALWIRE_JWT_TOKEN. The gate
flagged it as a hidden env knob, correctly: it gates authentication and nothing
documented it. Added to README.md and relay/README.md, stating that a JWT is an
ALTERNATIVE to the project/token pair rather than an addition to it.
relay/docs/getting-started.md said from_env() "reads exactly these three
variables", which B2 made false; corrected.

BEHAVIORAL-WIRE — a real key mismatch in the dump, not doc drift

token_interop returned valid=false against the oracle's true. B2 changed the
HMAC key semantics: the reference signs with secret_key.encode(), so the key is
the secret STRING's bytes, and the byte-vector constructor now hex-encodes its
input into secret_key_ so both spellings sign identically. tools/wire_dump.cpp
still built its manager with SessionManager(secret_bytes), which after that
change keys the HMAC with hex(kSecret) — while oracle_token() a few lines below
still signs with kSecret itself. Two different keys, so validation failed.

Fixed by constructing with the reference-shaped SessionManager(900, kSecret),
which is what makes this the cross-port interop check it claims to be.

STILL RED, VERIFIED PRE-EXISTING (not from this pass or B2)

ENVELOPE fails on 3 missing fixtures, not mismatches: compose_ctx_timeout_alone,
compose_abort_signal_alone, compose_ctx_and_signal_both. diff_port_envelope
counts pending toward failure (bad = fails + pending), and cpp's
envelope_dump.cpp has never emitted them — absent at da24e23 and now. Real
mismatches: zero. B2's only envelope_dump change was the mechanical
status()/status_code() rename.

SWAIG-HTTP-INVOKE, SECRET-SCRUB-LIVE and SECURE-DEFAULT want build/
swaig_http_dump, secret_scrub_dump and secure_default_dump. Those three programs
have no CMake target and no source in tools/ at either commit — they were never
written for cpp. Left for the owner; silencing them would hide missing coverage.

Verification: doc_truth 8/8 PASS (was 3 FAIL), behavioral BEHAVIORAL-WIRE PASS,
diff_port_surface exit 0 (2597 symbols), diff_port_signatures exit 0 (1522/1940),
construction_readback 217/217 exit 0, SURFACE suite 8/8 PASS, run-lint exit 0,
run-format no-op, 2034/2034 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1fEcuaivoN1poPGS75jAy
The B2 re-drift renamed SignalWireRestError::status() to status_code(), matching
the reference (signalwire/rest/_base.py:87 declares `self.status_code`) and every
other port: java/dotnet/go StatusCode, ts/php/ruby/perl statusCode, rust
status_code. cpp was the lone outlier on the bare name, and B2 surfaced it by
recording the attribute for the first time.

The rename swept the docs but MISSED THE COMPILED EXAMPLES. CI caught what the doc
gate structurally cannot:

  rest/examples/rest_video_rooms.cpp:31:36: error: 'const class
  signalwire::rest::SignalWireRestError' has no member named 'status'; did you mean
  'status_code()'?

12 example files called e.status(). All updated. Verified zero remaining.

WHY THE LOCAL GATE MISSED IT: doc_truth checks whether documented NAMES resolve
against the enumerated surface. It does not compile examples. So `all 8 rules PASS`
locally while a dozen examples were broken — the compile is a separate gate and a
separate build.

LESSON for the remaining B2 ports: a RENAME must be swept through compiled examples
and tests, not just docs. A fold (set_x -> x) is absorbed by the native-name
sidecar; a RENAME is not, because the old name exists under no spelling.

Not fixed here (pre-existing, proven at da24e23 where it also reports 55): the
bare-word DOC-AUDIT requalifies (at/back/begin/c_str — stdlib names needing
Class#method qualification). Same class as rust's known DOC_AUDIT_IGNORE backlog;
flagging rather than silencing.

doc_truth 8/8 PASS after the change.
…real defects

cpp had NEITHER security fixture and NEITHER rule wired, so neither gate had ever
run against it. Both now exist, are wired, and are proven non-vacuous. Wiring
them found three real defects, all fixed here.

WHAT IS NOW PROVEN THAT WAS NOT BEFORE

  SECURE-DEFAULT (per-PR)   a tool defined with no explicit `secure` argument is
                            SECURE, and the WIRE reflects it: the rendered SWAIG
                            webhook carries the per-tool __token iff the tool is
                            secure. Compared per-fixture against the python
                            oracle by diff_port_secure_default.py.
  SECRET-SCRUB-LIVE (nightly) driving the RELAY client through a real connect +
                            an inbound authorization.state re-auth frame at debug
                            level with the fixture sentinels leaks none of them
                            into the log. The static SECRET-SCRUB grep proves the
                            SHAPE; this proves the RUNTIME.

DEFECT 1 — define_tool defaulted secure=FALSE (security defect)

ToolDefinition::secure, AgentBase::define_tool, Service::define_tool and
SkillBase::define_tool all defaulted `secure = false`. The reference defaults
secure=True (tool_mixin.py:36), so every cpp tool a caller defined without
naming the argument shipped UNAUTHENTICATED. Flipped to true in all four.

DEFECT 2 — the render never MINTED the token, so `secure` had no wire effect

`secure` produced only an invented `"secure": true` key on the rendered function.
That key is not a property of the SWML UserSWAIGFunction schema and the reference
never emits it, so it was invented wire surface that the platform ignores — the
flag was decorative. The actual mechanism (reference agent_base.py:1040/1096-1100)
is `if func.secure and call_id: url_params["__token"] = token`, appended to the
function's web_hook_url. Implemented: build_swaig_functions now mints the
per-tool token via SessionManager and appends `__token=`, the invented `secure`
key is gone, and the request's `call_id` query parameter is threaded
render_swml_for_request -> render_swml_internal -> build_ai_verb ->
build_swaig_functions (reference: swml_service.py:807 reads it from exactly
there). handle_request now parses the URL's query string instead of passing an
empty map, so the served path gets the call_id too.

call_id is deliberately NOT a defaulted parameter on those three internal
builders. It was, briefly, and the default silently swallowed a missed
thread-through at one call site: everything compiled and every secure tool
rendered WITHOUT its token. Omission is now a compile error.

DEFECT 3 — the dispatcher validated the WRONG wire field

handle_swaig_request read `body["meta_data_token"]` and fed it to
validate_token. `meta_data_token` is the SWML meta_data SCOPING token
("Scoping token for meta_data ... Default is set by SignalWire"), not a
credential — a different field with a different purpose. The reference reads the
security token from the QUERY STRING (agent_base.py:1414:
`request.query_params.get("__token") or request.query_params.get("token")`).
Fixed to read `__token` with `token` as the reference's fallback spelling. This
is the same value the render now mints onto the webhook, so the two halves are
one mechanism; a new test extracts the rendered token and asserts it validates
for its tool+call_id and not for another call_id.

NON-VACUITY (each gate was made to go RED by reintroducing its defect)

  SECURE-DEFAULT, direction A: default flipped back to false
    -> RED on define_tool_default_is_secure (secure_default_true false vs true)
  SECURE-DEFAULT, direction B: tokenize every tool, ignoring `secure`
    -> RED on define_tool_explicit_insecure (wire_reflects_secure false)
  SECRET-SCRUB-LIVE: a debug log of the raw connect frame (F3.1) + the raw
  inbound frame (F3.2)
    -> RED on all 3 sentinels (project / token / authorization_state leaked:true)

All probes removed; both gates back to PASS.

FIXTURES

  tools/secure_default_dump.cpp -> build/secure_default_dump
  tools/secret_scrub_dump.cpp   -> build/secret_scrub_dump

Both at the paths _behavioral_commands.py declares for cpp. secure_default_dump
reads the recorded secure flag off the protected registry via a local subclass
rather than assuming it (and rather than adding a public getter, which would be
invented surface). secret_scrub_dump redirects fd 1 AND fd 2 for the whole drive
— a streambuf swap would miss anything writing to the descriptor directly, and
the vendored IXWebSocket transport can — then classifies the capture and forwards
it to the real stderr. It fails LOUD into that capture if the connect frame never
reached the server or the re-auth frame was never pushed, so a drive that did not
actually happen cannot report leaked:false vacuously. Its scratch file lives in
the repo-local .sw-tmp/, never a machine-wide temp dir.

SWEEP

Two tests asserted the invented `secure` key; both rewritten to pin the real
contract (token present for a default tool, absent for secure=false, absent
without a call_id) plus the render->validate round trip. All 2037 tests pass; the
examples target builds clean. Four doc sites stating `secure` defaults to false
corrected.

NOT ADDRESSED (pre-existing, unrelated, red before this branch too):
PACKAGE:ARTIFACT-DENY, ROOT-HYGIENE (port_surface_native.json at the root),
PUBLIC-JARGON (an 'oracle' mention in an agent_base.hpp comment from the B2 pass).
Also noted: cpp's run-ci does not wire ENVELOPE, BEHAVIORAL-STRICT-RENDER,
SECRET-SCRUB (static), TLS-VERIFY, CA-VAR or SWAIG-HTTP-INVOKE, and
build/swaig_http_dump still does not exist — out of scope here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…rink

A port emitting `any` silently satisfies every type the reference declares —
compare_param treats `any` on EITHER side as matching anything, and says so in its own
docstring. That is a fair accommodation for a dynamic language and an unlimited opt-out,
and until now nothing counted how much of it there was.

It is not academic. ConciergeAgent.__init__ declares
hours_of_operation: optional<dict<string,string>> in BOTH its method signature and its
section-10 construction node. go shipped a single STRING — per-label hours unreachable,
and global_data carrying a string where every other port carries a map — with no gate red
anywhere. The oracle stated the type correctly and nothing enforced it.

Wired per port at its measured count: php 176, cpp 122, ruby 97, rust 55, typescript 45,
dotnet 19, go 16, java 13, perl 13. Total 556 slots where a reference declaration is
currently unenforceable.

RATCHET, DELIBERATELY, NOT A HARD GATE. Failing nine ports at once would produce nine
allow-list entries, which is the exact opposite of the goal. Banking the current number
means the count can be driven down on purpose while it can never grow by accident.

Placed immediately after each port's SURFACE gate, because it reads the
port_signatures.json that enumeration writes. cpp needed a hand edit: it runs gates
SERIALLY via `run_gate "NAME" "desc" cmd` rather than the `sched_gate` DAG the other eight
use, so a scripted insert anchored on sched_gate correctly skipped it rather than
producing a silently dead line.

Verified all nine: bash -n clean, and the gate PASSES at its banked ratchet. Non-vacuity
proven rather than assumed — lowering a ratchet by one makes go, java and dotnet each exit
1 naming the regression, so the gate fires instead of decorating.
…ning them

Completes the fix for dotnet's "311 Python symbol(s) missing from port" — a CI-only red
that exited 0 locally against byte-identical inputs, against a port that was correct.

Root cause: enumerate_surface.py reaches the reference oracle through generate_rest.py,
which needs PyYAML. The CI interpreter had none, the import raised ModuleNotFoundError, a
bare `except Exception: return None` swallowed it, the oracle loaded EMPTY, and 266
oracle-gated members silently failed to emit. Proven by md5-ing every diff input (only the
REGENERATED port_surface.json differed) and then directly: 3.13 emits 1530 members, a
yaml-less interpreter 1264, and the 266 lost are exactly the gated ones.

porting-sdk 3eee516 added scripts/requirements.txt as the single declaration plus
check_script_deps.py to keep it honest. This is the consumer half: every workflow that
runs those scripts now installs from that manifest.

17 workflows across 9 ports — each port's surface-audit and doc-audit, plus go's
multi-os. Every one previously ran the gate scripts and installed NOTHING, which is why
the failure was one forgotten dependency away in nine repos simultaneously.

perl/live-smoke.yml was deliberately SKIPPED: it has no setup-python step to anchor to,
so an inserted pip line could not be guaranteed to have an interpreter. Flagged rather
than forced.

Placement verified programmatically, not by eye, because I got it wrong twice: the first
attempt split the setup-python step in half (install landed between `- name:` and its
`uses:`), and the second placed it AFTER the enumerate step in 7 workflows and BEFORE
setup-python in 4. Both passed `yaml.safe_load` — structurally valid and semantically
useless — which is exactly why the check asserts ORDER: install must come after any
setup-python step and before the job's first run step. All 17 now satisfy that, with no
duplicates and valid YAML across all 63 workflow files.
ARTIFACT-DENY correctly failed: the class-B2 re-drift (49d6ab8) added
port_surface_native.json — the sidecar DOC-AUDIT reads so an accessor fold does not break
cpp's own correct doc examples — without the matching allow entry.

That is not paperwork. cpp's shipped-file set is a `git ls-files` proxy with NO
include/exclude discipline, so an unlisted root artifact would ship inside a published
package. The gate's own message says it: "a published package must ship the library, not the
audit pipeline."

Entry added in the same audit-pipeline-not-library category as port_signatures.json,
port_signatures.baseline.json and port_surface.json directly above it. go, java, php and
dotnet each already carry the equivalent entry for their own sidecar; cpp was the one port
that gained the file without gaining the entry.

Verified: ARTIFACT-DENY reports "clean" and PACKAGE passes.
… B2 re-drift reds

Three reds on PR #53, all three tracing to the class-B2 re-drift (49d6ab8 / 452801a),
which introduced port_surface_native.json and a jargon-carrying header comment without
the companion declarations.

1. doc-audit job -- the CHECK was wrong, not the docs.
   .github/workflows/doc-audit.yml invoked audit_docs.py WITHOUT --native-names, so the
   8 folded setters it flagged (set_route / set_port / set_host / set_venue_name /
   set_amenities / set_faqs / set_suggest_related / set_signing_key) were reported
   "unresolved" even though every one of them is a real, declared, compiled cpp method
   (include/signalwire/swml/service.hpp:74,84; include/signalwire/prefabs/prefabs.hpp:177-178;
   include/signalwire/agent/agent_base.hpp:614). The surface enumerator FOLDS those
   accessors onto the reference attribute names, and the sidecar carries the native
   spellings so a correct doc example still resolves.

   The local suite already got this right: porting-sdk scripts/suites/_doc_audit.py
   passes --native-names automatically for any port shipping the file, which is exactly
   why DOC-TRUTH was 8/8 PASS locally on 5dfef63 while this job failed on the same tree.
   Local and CI disagreeing about one tree is the defect. Fixed by passing the flag in
   both invocations (audit + the on-failure JSON report), matching the prior art php
   landed for the identical gap.

   Proof: audit_docs.py without the flag exits 1 with those 8 names; with it, exit 0 and
   "docs/examples reference only known symbols (2898 resolved / 3443 total)".

2. ROOT-HYGIENE -- port_surface_native.json was a tracked repo-root file with no
   allowlist entry. Same shape as the ARTIFACT-DENY entry added in 5dfef63: the file
   cannot move to eng/ because the shared consumer resolves it at
   repo_path / "port_surface_native.json" by that exact name. java, dotnet and php
   each already carry the equivalent entry; cpp gained the file without it.

3. PUBLIC-JARGON -- agent_base.hpp:823 leaked 'oracle' into a shipped header comment
   ("the oracle's class-B2 rule now records them"). Rewritten to say the useful thing in
   user-facing terms: those two accessors are public because the configuration they read
   is part of the public API. Not allowlisted -- the comment was genuinely internal
   vocabulary in a public header.

Verified: bash scripts/run-ci.sh exit 0, all per-PR gates PASS (ROOT-HYGIENE and
PUBLIC-JARGON both now "clean"); run_tests 2037/2037; cmake --build --target examples
exit 0.
SNIPPET-COMPILE (nightly tier) failed on 3 relay doc snippets referencing
Message::from and Message::to. Those members do not exist: the field was
deliberately named from_number / to_number to match the wire keys it is read
from (include/signalwire/relay/message.hpp:40-45 records the reasoning --
"the shorter from/to was a gratuitous divergence from the wire key"). The
rename swept the code, tests and examples, but relay/docs/ was missed, so the
docs promised a phantom API.

5 references fixed across relay/docs/client-reference.md and
relay/docs/messaging.md. This is the compiled-doc class of drift the doc_truth
gates cannot catch on their own -- DOC-AUDIT checks documented NAMES and never
compiles anything, which is why 8/8 DOC-TRUTH stayed green over a snippet that
would not build.

Also fixed the same file's play/wait snippet, which discarded the [[nodiscard]]
bool from Action::wait(). That attribute exists precisely because the bool
reports whether the action completed
(include/signalwire/relay/action.hpp:94-97), so the snippet was modelling the
bug the attribute guards against; it now branches on the result.

Verified: snippet_compile.py --port cpp exit 0, "cpp: clean (523 compiled)"
(was "3 compile failure(s)"); doc_truth.py exit 0, "all 8 rules PASS".
…est binary

A local run-ci TEST gate died with exit 139 (SIGSEGV) after all 2037 tests had
reported OK. The crash point is identifiable from the truncated output: the run
stopped immediately after the two TLS mock skips, i.e. inside
tls_sdk_sslserver_verified_by_client (tests/test_tls_server_https.cpp:62) --
the one test that runs Service::serve() on its own thread and calls stop() from
a scope guard.

The mechanism is a genuine data race in shipped library code, in three classes
with identical shape (Service, AgentBase, AgentServer):

    serve()  [server thread]        stop()  [owner thread]
      server_ = make_http_server()    if (server_) {
      setup_routes(*server_)            server_->stop();
      server_->listen(...)  <-- blocks  server_.reset();   <-- destroys it
                                      }

server_ was a plain unique_ptr with NO synchronisation, written by one thread
and read/reset by another. Two distinct faults:

  1. Unsynchronised access. stop() could observe a half-assigned pointer, or
     reset it while serve() was still in setup_routes().
  2. Use-after-free. Even correctly ordered, reset() DESTROYS the server while
     serve() is still executing inside listen() on it. httplib's stop()
     unblocks listen(), but the object is gone by then.

Fix, applied uniformly to all three:
  - server_ becomes a shared_ptr. serve() keeps its own strong reference for the
    duration of listen(), so a concurrent stop() dropping the member can no
    longer destroy the server out from under the thread inside it. The object
    dies when whichever of the two finishes second releases its reference.
  - The member is written/dropped under a mutex, and the lock is RELEASED before
    the blocking listen() -- holding it across listen() would deadlock every
    stop(). Service and AgentBase get a new server_mutex_; AgentServer reuses
    its existing live mutex_ (verified non-recursive: setup_routes' own body
    takes no lock, and the lock_guard in the /agents handler is inside a
    deferred request lambda).
  - AgentServer::serve() now snapshots agents_.size() under the same lock
    instead of reading the container after releasing it.
  - The cert/key-failure early return now resets server_ instead of leaving a
    live-but-invalid server published on the member.

Both AgentBase::serve() and Service::serve() are threaded by the existing suite
(tests/test_served_routing.cpp:71, tests/test_tier2_behavioral.cpp:307,
tests/test_tls_server_https.cpp:86), so all three sites were reachable, not
theoretical. server_ is private in every case -- no public surface change.

Note for a follow-up (not touched here): AgentBase declares a
mutable std::shared_mutex state_mutex_ that is never used anywhere, and
Service's <mutex> include was previously dead. state_mutex_ is still dormant.

Verified: run-tests.sh exit 0, 2037/2037 passed; run-ci.sh exit 0, "==> CI PASS"
(all per-PR gates including SURFACE and SEMVER-DIFF).
…ckConfig

Coordinated pass against porting-sdk 3435180 (169 $defs = the prior 167 +
RingbackConfig + AiSidecar). Two upstream changes land together on purpose: doing
them as two separate 9-repo passes would burn the fan-out twice.

  1. RUNTIME schema — this SDK SHIPS schema.json for its own validation, so a stale
     copy means the shipped library validates against a schema with no ai_sidecar and
     no RingbackConfig. Re-vendored byte-for-byte from canonical (copied, not
     re-serialized: the x-sdk-* markup is load-bearing and a re-serialize would also
     churn formatting).
  2. GENERATED types — regenerated from CANONICAL, which is what the generators read
     (NOT the vendored copy). That is why GEN-FRESH-SWML went red the moment canonical
     moved: it compares generated code against canonical.

New surface: the ai_sidecar verb (live_transcribe plus an LLM/SWAIG/MCP loop) with
AiSidecarConfig carrying all 12 fields — real direction/customer_role enums and SWAIG
resolved through its $ref. RingbackConfig replaces what was collapsing to an untyped
map: connect's ringback is now `array | RingbackConfig` with all 9 properties typed.

VERIFIED, by running the gates rather than reading the diff:
  * GEN suite: all rules PASS (this port, and 9/9 across the fleet)
  * SURFACE-DIFF: exit 0 — the 6 drifts the oracle correctly raised
    (AiSidecarConfig.SWAIG + 5 ringback slots) are now implemented, not excused.
    No omission entry was added: a real missing capability is never idiom.
  * vendored copy asserted equal to canonical, 169 defs, x-sdk-* count 5
  * behaviorally exercised against the vendored schema (go, ValidateVerb):
    ai_sidecar{prompt,lang,direction,customer_role} -> valid;
    missing `prompt` -> correctly rejected; customer_role "nobody" -> correctly
    rejected; ringback object form -> valid.

port_signatures.json is regenerated and committed — SURFACE-FRESH fails otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…ackConfig surface

Follow-up to the 169-def schema adoption in this PR. There are TWO per-port audit
artifacts and the schema commit only refreshed one: port_signatures.json was
regenerated, port_surface.json was not — so surface-audit went red naming the same 4
symbols the signatures gate had already accepted:

    signalwire.core.swml_verbs_generated.AiSidecar
    signalwire.core.swml_verbs_generated.AiSidecarConfig
    signalwire.core.swml_verbs_generated.AiSidecarConfig.SWAIG
    signalwire.core.swml_verbs_generated.RingbackConfig

Regenerated with the same invocation CI uses. Verified: diff_port_surface.py exits 0
against the regenerated porting-sdk/python_surface.json (porting-sdk c08cb73), for this
port and all nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…a loaded instead

ai_sidecar makes 39 verbs, so every `== 38` assertion in the fleet broke at once. The fix
is not 38 -> 39: that just moves the same trap one verb further out, and the next upstream
verb breaks it again.

WHY THESE ASSERTIONS SHOULDN'T EXIST AS EXACT COUNTS: the python reference has NO
equivalent assertion. swml_service.py only LOGS the count
(`log.debug("found_verbs_in_schema", count=len(verb_names))`). So an exact-count check is
not parity with the reference — it is port-invented brittleness that fires on every
correct upstream change and has never caught a real defect. Notably php/perl/dotnet/cpp
already used `>= 38` and were unaffected, which is the shape that survives.

What is actually worth pinning, and what these now assert:
  * the schema is not TRUNCATED (>= 38 verbs — catches a failed/partial load, the real bug)
  * the count agrees with the names actually extracted (catches a count/list mismatch)
  * ts: names are unique; and the pre-existing loop that every schema verb has a builder
    method — that loop was always the real test, the headcount was noise.

DELIBERATELY LEFT EXACT: SwmlTest.java:358 `assertEquals(38, ...getVerbs().size())`. That
counts the 38 verbs the test itself explicitly calls, so it is a genuine invariant of the
test body, not a schema headcount. Changing it would weaken a real check.

Verified per port by running the tests, not by reading:
  go    pkg/swml ok
  ruby  64 runs, 117 assertions, 0 failures
  ts    2 files, 84 tests passed
  java  SchemaUtilsTest + SwmlTest, 0 failures

Scope note: all of go's THREE red checks (test, doc-audit, windows) were this same pair of
tests — doc-audit and windows both run `go test ./...`. Likewise ruby's REST-COVERAGE red
was the same single test failure surfacing again, because that gate runs the whole suite.
Four ports, three gates, one root cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
@mjerris
mjerris merged commit 98e7245 into main Jul 27, 2026
3 checks passed
@mjerris
mjerris deleted the chore/allowlist-fold-cpp branch July 27, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coordinated-pass Coordinated cross-port fold pass

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant