fix(ci): green the gates the ai_chat/ChatGateway commits reddened - #81
Merged
Conversation
The three ChatGateway commits (8b790ea, cac3118, 20663a6) landed on main without a full run-ci, reddening LINT, FMT, TYPECHECK and NO-CHEAT. This fixes each at its source. LINT (ruff 0.15.21, the pinned version) - ai_chat/__init__.py: RUF022 __all__ sorted. - search/document_processor.py: SIM102 nested if collapsed. FMT - ruff format over ai_chat/gateway.py + core/function_result.py. TYPECHECK (mypy --strict; the config puts tests in scope on purpose: "a new untyped test fails the gate") - tests/unit/ai_chat/test_gateway.py shipped fully unannotated: 44 no-untyped-def + 19 no-untyped-call. Annotated throughout. - FunctionResult.response widened to `str | dict[str, Any]`, so 37 call sites doing `.response.lower()` stopped type-checking. Added `assert isinstance(<r>.response, str)` next to the existing `assert isinstance(<r>, FunctionResult)` — a real assertion that narrows the union, not a cast. - FunctionResult.hold: `bool` subclasses `int`, so excluding bools from the back-compat int-swap left `str | bool`. Handle bool explicitly; the remaining type is `str | None`. hold(120) still means hold(timeout=120). - ChatGateway.visible_messages / last_activity accept None and non-dict items by design (`for msg in messages or []`, `if not isinstance(msg, dict): continue`) and are tested for it, but were typed `list[dict[str, Any]]`. Widened to match the real, documented contract. Free to change: ChatGateway is new surface no port has implemented yet. NO-CHEAT - Three origin tests asserted nothing ("does not raise"), so they passed regardless of the code. Each now pairs the allowed case with the refusal that proves it is an exemption and not open-by-default: localhost vs an unlisted origin, a listed origin vs a lookalike domain, absent vs present-but-unlisted. Verified: LINT clean, FMT clean, NO-CHEAT clean, mypy clean over every file CI reports, 5940 unit tests pass. The 6 remaining mcp_gateway failures are pre-existing (they fail identically on unmodified main) and env-dependent — CI passes them. Not addressed here (deliberately): GEN-FRESH and DRIFT/SEMVER-DIFF are coordinated-pin artifacts. PORTING_SDK_REF is set to wave6/ctor-dunder-fold, so CI builds against that branch; the matching regen is PR #78's half of the wave, not this branch's.
…pecs CI resolves porting-sdk via PORTING_SDK_REF, currently wave6/ctor-dunder-fold, so GEN-FRESH regenerates from THAT branch's specs and compares. The committed files were generated from main's specs, so six reproduced differently and the gate failed. Regenerated with the pinned ref's specs; `--check` is now clean. Note these are NEWER than the same files on the wave6 branch itself: the swaig specs gained `| str` on several action fields after that branch last regenerated (e.g. `consolidate: bool` -> `bool | str`, `wait: bool` -> `bool | str`). So this is the output current wave6 specs actually produce, which is what CI checks against. Full unit suite still 5940 passed; the 6 mcp_gateway failures are pre-existing and env-dependent (they fail identically on unmodified main).
This was referenced Aug 10, 2026
anthmFS
added a commit
that referenced
this pull request
Aug 13, 2026
) * docs(rest)+test(core): true live_transcribe call shape; drop stale ai_sidecar bypass calling.md showed live_transcribe(call_id, action="start", lang="en"), but the generated signature takes action as a keyword-only TypedDict union ({"start": {...}} or the literal "stop") and has no lang/from_lang /to_lang parameters — copying the documented call raises TypeError. The examples now show the real shape, with the start parameters inside the action object where the schema defines them. The sidecar pattern test appended ai_sidecar to the raw document with a comment that the verb was not in the live SWML schema yet. It landed in 4645d48, so the test now uses add_verb_to_section directly — the path the comment promised, and stronger, since the verb goes through schema validation instead of around it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgA3KeCEPMKMJVvroZY1wV (cherry picked from commit 77fc34c) * fix(ai_chat): forward conversation_timeout on the start path prepare() puts conversation_timeout into the start params, but the HTTP dispatch rebuilt the create_conversation call with only id and config_url, silently dropping it. A gateway configured with conversation_timeout=900 told the browser 900 (via effective_timeout) while the service kept its own 3600 default — the page schedules its idle warning around a number the service never enforces. The chat path was unaffected (raw_post streams params verbatim), which kept the drift invisible for conversations opened by a first message. The regression test drives the real HTTP dispatch through the ASGI harness — prepare() was already correct, so a prepare()-level assertion would have passed with the bug in place. Verified red without the fix, green with it; it also pins the auto-create chat path so the two paths cannot drift apart again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgA3KeCEPMKMJVvroZY1wV (cherry picked from commit 5bf93c7) * fix(FunctionResult): emit execute_swml transfer beside the SWML document execute_swml(transfer=True) wrote the flag INSIDE the SWML document — {"SWML": {..., "transfer": "true"}} — where it is not a SWML key, so the document executed but the call never exited the agent. The platform documents transfer as a sibling of the SWML key in the action object, which is exactly the shape the live-proven connect() and swml_transfer() helpers already emit. The action is now {"SWML": <doc>, "transfer": "true"}; transfer=False still omits the key, and the caller's dict is still never mutated. The three tests that pinned the inside placement now pin the sibling placement, one of them asserting shape-parity with connect()'s action so the two paths cannot drift apart again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgA3KeCEPMKMJVvroZY1wV (cherry picked from commit 295c17e) * fix(FunctionResult): tap direction is speak/listen/both, and always emitted The tap() helper disagreed with the SWML schema (and the platform docs) twice over. It accepted and emitted "hear", which is not a tap direction — the verb's enum is speak/listen/both, exactly the set record_call() already uses — so direction="hear" produced SWML the platform rejects; it now raises the existing ValueError instead of silently emitting a dead tap. And its omit-if-default logic assumed the verb's default matches the helper's "both" when the verb actually defaults to "speak", so tap(uri) — documented as tapping both directions — produced a speak-only tap. direction is now always emitted; codec/rtp_ptime omissions stay (their helper defaults match the verb defaults). Note for the port audit: tap.direction's Literal feeds python_signatures.json in porting-sdk as enum<...>, so the oracle needs a regen alongside this change. docs/swaig_reference.md and docs/api_reference.md updated to the real enum (api_reference had a third variant — inbound/outbound plus a G722 codec and sip: URIs that the helper never accepted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgA3KeCEPMKMJVvroZY1wV (cherry picked from commit 0851a94) * fix: annotate the new gateway test; salvage the search_service import Two small follow-ups to the four fixes cherry-picked above. The `conversation_timeout` test arrived unannotated. mypy has `tests` in scope deliberately ("a new untyped test fails the gate"), so it reds TYPECHECK on main. Annotated; mypy is back to main's exact baseline. `from __future__ import annotations` in search_service.py is the one line worth keeping from #83, which was otherwise superseded by #81. Measured on its own: no change to the mypy count, so it is hygiene rather than a fix. --------- Co-authored-by: grandcamel <jasonkrue@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
hey-august
pushed a commit
to hey-august/signalwire-python
that referenced
this pull request
Aug 17, 2026
…s + GEN-FRESH gate (signalwire#39) * feat(types): spec-generated REST/RELAY TypedDicts + wire method return types + GEN-FRESH gate Adds field-level static types to the REST/RELAY surface, generated mechanically from the canonical specs (the same approach the TypeScript port uses), with zero runtime change and zero cross-port drift. - 15 generated modules (rest/namespaces/*_types_generated.py + relay/ protocol_types_generated.py): one TypedDict per openapi/relay-protocol schema + per-operation Request/Response aliases. 1468 types. Emitted by porting-sdk/scripts/generate_python_rest_types.py. - 160 REST methods across 20 namespaces wired to their generated return types (e.g. fabric list_versions -> CallFlowVersionListResponse), guided by the TS port. Imported under TYPE_CHECKING with string forward-refs: ZERO runtime cost (the generated module is never imported at runtime) and the method bodies are unchanged — they still return the raw server JSON dict. A TypedDict is a plain dict at runtime, so a differently-shaped server response is returned unchanged and never raises. - GEN-FRESH gate in run-ci.sh: `generate_python_rest_types.py --check` fails if any committed generated module no longer reproduces from its spec (mirror of the TS port's --check). Gates: mypy --config zero, ruff format + check clean, GEN-FRESH pass. All 9 ports remain drift=0 (the complex types are matched cross-port by the porting-sdk checker's generated-type normalization; dict-recording ports match via gen<->dict). Depends on porting-sdk: generator + checker rules + GEN-FRESH script must be on porting-sdk main first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(coverage): omit spec-generated type stubs from coverage The 15 generated *_types_generated / protocol_types_generated modules add ~1500 type-only statements (imported solely under TYPE_CHECKING, never executed), which dragged total coverage from ~74% to 55% and tripped the 63% fail-under. They carry no testable runtime code and are policed by the GEN-FRESH gate, not by tests — omit them from coverage measurement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * wip(types): generic CrudResource + 26 resource bindings (dual-rep) WIP checkpoint (#81): CrudResource[TList,TItem,TCreate,TUpdate] generic base; 26 REST resources bound to spec-generated types; AutoMaterializedWebhook.create honestly returns dict (intermediate orphan-create); coverage omits generated stubs. mypy clean, deprecation tests pass, 9/9 ports drift=0. NOTE: ruff check still flags forward-ref TYPE_CHECKING imports as unused (fix pending). Local checkpoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * lint: exempt F401 on REST namespace files (forward-ref-only imports) The generated-type imports under TYPE_CHECKING are used only inside quoted forward-refs (return annotations + CRUD-base subscript bindings), which ruff's F401 can't see -> false 'unused import' + --fix strips them, breaking mypy. Scoped per-file-ignore (mypy + DRIFT gate prove the imports are real). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rest: honest base create/update + move FabricResource bases to _base Prepares the REST client for generated typed CRUD resources. - CrudResource.create/update: drop the broken `**kwargs: TCreate/TUpdate` (which typed each kwarg VALUE as a whole request dict) for an honest `**kwargs: Any -> TItem` fallback. The closed typed shape lives on the generated per-resource subclasses; the class-level CrudResource[...] binding (untouched) is what publishes the real TCreate/TUpdate to the oracle, so the structural crud_base{bind:[4]} is unchanged and all ports stay drift=0. - Move FabricResource / FabricResourcePUT from the fabric namespace into _base (they are generic CRUD bases) so the generated fabric_resources_generated subclasses can inherit them without an import cycle. fabric.py re-imports them; they remain importable from there. These intermediate generics are not recorded in the oracle, so the move is drift-neutral. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest/fabric: generated typed CRUD resources (closed create/update + extras) Wire the generated per-resource typed CRUD subclasses into the fabric namespace and update the tests for the now-enforced spec-required fields. - fabric_resources_generated.py (generated): 12 typed CRUD subclasses, one per full-CRUD fabric resource, each bound to its spec types with a closed create/update — explicit spec fields (required honored) + an explicit `extras` door, no **kwargs tail. The named-subclass shape the oracle records as a crud_base; the structural binding is unchanged so parity holds. - fabric.py: construct the generated subclasses (group-A: ai_agents, sip_gateways, the script/connector/endpoint families, webhooks) instead of bare FabricResource[...] instances. Remove AutoMaterializedWebhook + its deprecation warning — these SDKs are pre-release, so direct webhook create is just a normal operation (no back-compat to deprecate). - tests: the typed create now enforces spec-required fields at the signature, so the coverage suites pass complete bodies (per-resource _CREATE_BODY/_UPDATE_BODY maps); error tests send a valid body so the 422 comes from the server, not the client; webhook tests assert no deprecation warning. Add TestRequiredFieldEnforcement: each missing required field raises TypeError, a typo'd field is rejected, and `extras` merges unknown fields into the body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: generated CRUD resource modules for datasphere / relay-rest / video Output of the x-sdk-resource-driven generator for the non-fabric CRUD namespaces, plus a regenerated fabric module (PUT resources now carry _update_method = "PUT" rather than extending a separate FabricResourcePUT base). These modules are generated and mypy/ruff-clean but NOT yet wired into their namespace files — wiring (and deletion of the now-fully-generatable hand classes) follows once the per-resource sub-resource methods are generated too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate resource modules with per-resource sub-methods datasphere/relay-rest/video resource classes now include their declared sub-resource methods (search, list_chunks, list_members, list_streams, etc.) in addition to the base CRUD surface. Still generated-but-not-yet-wired; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate resource modules with spec-composed base paths Each generated resource class carries an __init__ setting its base URL path (namespace prefix + collection); construction is Resource(http). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: wire generated QueuesResource; fix fabric construction for baked-in paths - queues.py now re-exports the generated QueuesResource (the hand class is fully covered by the generated CRUD + sub-methods). - fabric.py: generated resource classes now bake their own base path into __init__, so construct them as Resource(http) (drop the path arg); the still-hand-written classes (call_flows/conference_rooms/subscribers/cxml_applications) keep (http, base). - Regenerated resource modules with the collection-relative / sibling path fix. All 766 rest tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: wire generated CRUD resources for datasphere / relay-rest / video Replace the hand-written resource classes with the generated ones (which cover the full CRUD surface plus their declared sub-methods): - queues / number_groups / verified_callers: re-export the generated class. - datasphere: DatasphereNamespace constructs DatasphereDocumentsResource (back-compat alias DatasphereDocuments). - video: rooms / conferences use VideoRoomsResource / VideoConferencesResource (aliases VideoRooms / VideoConferences); the other video resources stay hand-written. - fabric: construct the generated resources as Resource(http) (they bake their own base path); hand group-B classes keep (http, base). The generated resource modules and all wired namespace files are ruff + mypy clean and GEN-FRESH passes. KNOWN: ~15 pre-existing CRUD tests fail because the generated typed create/sub-methods enforce the spec's required fields and field names, while the tests were written against the old **kwargs and used wrong/partial fields (e.g. phone_number vs the spec's number). These are replaced by the generated tests in the next step; the failures are stale tests, not wiring bugs (verified). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: ReadResource base; generate + compose the logs namespace from product specs - _base.py: add ReadResource (list + get); CrudResource extends it. - logs.py: thin convenience namespace composing the generated per-product log resources (MessageLogsResource/VoiceLogsResource/FaxLogsResource from the messaging/voice/fax specs, ConferenceLogsResource from the logs spec). The hand classes are deleted; back-compat aliases kept. - Generated read-only / method-only resource modules. logs tests pass (read-only resources have no typed-create enforcement issue). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate relay-rest module with non-CRUD resources addresses/recordings/short_codes/sip_profile/imported_numbers/mfa/lookup now generated (BaseResource + declared methods). Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: generate the calling resource (37 typed command-dispatch methods) Generated from the calling spec's CallRequest discriminator mapping; each command is a typed method posting {command, params, id?}. Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate relay-rest with phone_numbers set_* helpers The 7 call-routing convenience helpers (set_ai_agent/set_cxml_webhook/etc.) now generated as typed wrappers over update(). Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: generate registry + project/chat/pubsub token resources registry (brands/campaigns/orders/numbers) and the project/chat/pubsub token resources now generated; create ops with union bodies carry a typed body param. Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate fabric + video with the completed non-CRUD markup fabric group-B sub-methods, GenericResources/FabricAddresses/FabricTokens/ CxmlApplications, and the video sub-resources (sessions/recordings/tokens/streams/ conference_tokens) now generated. Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: wire single-resource namespaces to generated classes (delete hand classes) Replace the hand-written resource classes with the generated ones (which now cover the full surface including sub-methods and phone_numbers' set_* helpers): - addresses/recordings/short_codes/sip_profile/imported_numbers/lookup/mfa/ phone_numbers re-export from relay_rest_resources_generated. - chat/pubsub re-export from their generated modules. - project: ProjectNamespace constructs ProjectTokensResource (back-compat alias ProjectTokens). All routes resolve and methods are present (verified end-to-end). The remaining ~41 failing tests are all the benign typed-enforcement category (stale tests passing wrong/partial field names to now-typed methods) — no wiring/route bugs; replaced by generated tests in #10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: wire all remaining namespaces to generated resources (delete hand classes) - registry: RegistryNamespace constructs the 4 generated registry resources. - calling: re-export the generated CallingResource (37 command-dispatch methods); back-compat alias CallingNamespace. - video: VideoNamespace constructs all 7 generated video resources (rooms/conferences + room_tokens/room_sessions/room_recordings/conference_tokens/streams). - fabric: FabricNamespace constructs the generated group-B (call_flows/conference_rooms/ subscribers/cxml_applications) and root resources (resources/addresses/tokens); all hand resource classes deleted, back-compat aliases kept; list_addresses now routes the singular sub-path for call_flows/conference_rooms. All routes resolve and methods/sub-methods are present (verified end-to-end). The remaining failing tests are all stale tests of deleted hand-class behavior (typed- enforcement kwargs, removed deprecation warnings, the removed cxml_applications.create stub which never had a spec route) — no wiring/route bugs; replaced by generated tests in #10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: bare resource class names; drop all back-compat aliases Regenerate with bare class names (Queues, Subscribers, VideoRooms, ... — no Resource suffix). Update the wiring (client.py + namespaces) and rest tests to the bare names. Delete all 21 hand-written back-compat aliases; the one kept name, CallingNamespace, is now a generated alias (x-sdk-resource.aliases on the calling resource). Full rest suite failure count unchanged (98, all the pre-existing benign typed- enforcement stale tests) — the rename introduced no new breakage. GEN-FRESH passes; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: generate PhoneCallHandler from spec (13 values); drop CallingNamespace alias - PhoneCallHandler is now generated from the relay-rest spec's call-handler enum (13 values, in sync with the wire) and re-exported from signalwire.rest; the hand-written call_handler.py (11 values) is deleted. - calling: drop the CallingNamespace alias; the resource is `Calling` everywhere (client.py constructs Calling; tests updated). Full rest suite failure count unchanged (98 — the pre-existing benign typed-enforcement stale tests); no new breakage. ruff + mypy clean; GEN-FRESH passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: generate the client object tree; client.py composes it client.py now inherits the generated _GeneratedResourceTree and calls _wire_resources() instead of 21 hand wiring lines — it owns only auth + the (pending-removal) compat namespace. The full tree (client.queues, client.fabric.ai_agents, client.video.rooms, client.logs.messages, ...) is generated from each resource's spec placement. Full rest suite failure count unchanged (98 benign); no new breakage. ruff + mypy clean; GEN-FRESH passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: delete the 20 redundant hand namespace files (superseded by generated tree) The hand namespace files (fabric.py/video.py/queues.py/...) — container classes and resource re-exports — are fully superseded by the generated _client_tree + the *_resources_generated modules, and nothing imports them anymore. Delete all 20; the one test reference (test_client) now imports the containers/Calling from the generated modules. namespaces/ is now generated modules + compat (the deliberate hand exception). Full rest suite failure count unchanged (98 benign); no new breakage. ruff + mypy clean; GEN-FRESH passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: green the suite — fix 98 test failures against the generated surface Align the REST tests to the generated SDK surface (resources are now generated from the specs). Done via a 24-agent test-fix pass + the two generator fixes: - Wrong/renamed fields -> the spec's actual field name (domain->domain_identifier, friendly_name->name, code->verification_code, destination->dest, phone_number->number, token->refresh_token, embed_id->token, name->display_name, ...). - Partial bodies in error tests -> the spec's required fields with valid placeholders (the 4xx still comes from the pushed mock scenario). - Removed hand behavior -> removed obsolete deprecation-warning assertions and the cxml_applications.create stub (no spec route). - Generator-surfaced fields now reachable: Mfa.sms(from_), Calling.dial(to/from_/url/ codecs/...), Calling.update(id/status) — tests updated to the typed surface (from_ param -> wire "from"; status not state). - Addresses.create now sends the 9 spec-required fields. Full rest suite: 765 passed (was 98 failing). No spec edits — real spec gaps were logged (the SPEC_AUDIT_vs_RAILS.md findings), none papered over. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: type the hand-written substrate so the whole rest space passes mypy --strict Annotate the last non-generated rest code — the runtime substrate the generated resources sit on: - _base.py: HttpClient (_request/get/post/put/patch/delete -> Any wire JSON), BaseResource (__init__/_path), and the CRUD base methods (cast the Any wire returns to the bound TList/TItem). SignalWireRestError.__init__. - _pagination.py: PaginatedIterator fully annotated (also merged the stray double module docstring so imports sit at the top). - client.py: RestClient.__init__ signature. - core/logging_config.py: get_logger(name: str) -> Any (it was untyped and used by 31 files; fixed at the source rather than ignored in rest). - regenerated relay-rest (set_* helpers drop the now-redundant cast). Result: mypy --strict is clean across all 31 generated + wiring + substrate rest files (compat.py excluded — it is being removed in the Twilio-compat removal). The configured project mypy stays clean; the 765 rest tests still pass; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: remove the entire Twilio-compat API (namespace, generated types, tests) Delete the compat surface completely: - namespaces/compat.py (the 12 Compat* hand classes) and namespaces/compatibility_types_generated.py. - client.py: drop the CompatNamespace import + the self.compat construction; RestClient is now purely the generated resource tree + auth. - tests: delete all 15 test_compat_*.py files and the TestCompat class in test_namespaces.py; drop the compat references/examples in test_client.py and conftest. - _base.py: drop the compat-subclass rationale from the update() comment (the positional- only resource_id stays — harmless and keeps the oracle signature stable). Full rest suite: 506 passed (the ~259 compat tests removed). No functional compat remains anywhere; mypy --strict clean (30 files); GEN-FRESH passes. The only residual "compat" mentions are generated wire-description docstrings (laml_call value sources), which are correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * relay: reuse Action bases (StoppableAction/PausableAction/VolumeAction); rest: regenerate - relay/call.py: collapse the ~14 repeated stop/pause/resume/volume action sub-methods into reusable bases parameterized by a _command_prefix class attr — StoppableAction (stop), PausableAction (+pause/resume), VolumeAction (+volume). Each *Action class now just declares its prefix + composes the right base (FaxAction sets the prefix per instance for send/receive). Behaviour preserved; the test asserting the old _method_prefix attr updated to _command_prefix. - rest: regenerate (no surface change from the BaseResource/default/positional generator work — default stays doc-only so the wire body is unchanged). mypy --strict clean on call.py + the generated modules; rest + relay suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * swml: wire generated _SwmlVerbs Protocol into SWMLBuilder (static verb typing) SWMLBuilder inherits the generated _SwmlVerbs Protocol under TYPE_CHECKING, so the verb methods it installs dynamically (from schema.json) are now statically typed — clears the verb-visibility strict errors (swml_builder 21 -> 7; remaining are the dynamic-creation internals, hand-annotation territory). Runtime behavior unchanged (Protocol is type-only); 416 SWML/builder tests + the full core/rest/relay suites green. Adds signalwire/core/swml_verbs_generated.py (generated from porting-sdk/schema.json). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * strict cleanup: non-generatable errors (311 -> 238), no generator could fix these A targeted pass over strict errors that no spec-generator would address: - abstract SWMLBuilder regression fixed (the _SwmlVerbs base is now a plain class with concrete bodies — see the generator commit). - Explicit re-exports (__all__) on _agent_host / _mixin_host so --strict no_implicit_reexport resolves AgentHost/_HostTyped (cleared the 9 attr-defined on the mixins). - type-arg (32 -> 0): bare dict/list/set/tuple/Callable/Pattern given their args across 16 files; auth_mixin gained its missing `from typing import Any`. - logging_config.py fully annotated (strip_control_chars/get_execution_mode/configure_logging/ the structlog processor + mode helpers) — a foundational file used by 31 modules, so its typed calls cascade-cleared a chunk of no-untyped-call package-wide. The remaining 238 are genuine framework-typing work (FastAPI route handlers + @tool/@app decorators + the deliberate _HostTyped TYPE_CHECKING/runtime-split tradeoff) — not spec-generatable, left for a dedicated framework-annotation effort. ruff clean; 5358 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * strict: whole source tree to mypy --strict clean (238 -> 0) + adopt strict as the gate A 5-agent parallel burndown of the remaining strict errors, with REAL types — never papered with bare Any (dict[str,Any] only where the value is genuinely heterogeneous, the honest equivalent of TS's Record<string,unknown>). Corrects an earlier wrong claim that this framework code "couldn't be typed/generated": TypeScript passes full strict on the same surface (web/agent/swml) with only 3 `any` in its src, so it clearly can. Key fixes: - The @AgentBase.tool() decorator is now properly typed (TypeVar bound to Callable), clearing untyped-decorator cascades SDK-wide. The untyped-decorator errors were NOT structural — they were a missing-return-annotation cascade (proved: a typed-return FastAPI handler is clean). - FastAPI route-handler return annotations are RUNTIME-LOAD-BEARING (FastAPI builds a response model from them; a union return crashes startup). Resolved with an _as_response helper: internal _handle_* methods honestly return Response | dict[str, Any] (the dict is a real SWAIG/post-prompt passthrough that tests assert on); decorated route handlers funnel through _as_response to stay -> Response. No behavior change. - Explicit __all__ re-exports (_agent_host/_mixin_host) for no_implicit_reexport; the _HostTyped "cannot subclass Any" is the documented TYPE_CHECKING/runtime split -> scoped # type: ignore[misc] with reason. flask/flask-limiter (optional extras, no stubs) -> scoped # type: ignore[untyped-decorator]. - type-arg/redundant-cast cleared; logging_config fully annotated (cascade-cleared callers). - service_loader: bytes(result.body).decode() — a real latent bug (memoryview has no .decode()) that strict surfaced. Config: tool.mypy now `strict = true` (was a hand-picked subset) so the gate matches the bar. `python -m mypy` passes at 0; ruff clean; 5358 unit tests pass. Note: tests/ are NOT yet in the type-check scope (76k LOC, ~9570 strict errors) — deferred to the test-generation effort (#10) which will replace much of that surface with generated, already-typed code rather than hand-typing throwaway. Pre-existing broken example (bedrock_agent_run.py imports a never-defined run_agent) is unrelated, left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: Python-only back-compat shims for the renamed/moved REST symbols When the REST layer became spec-generated, 20 hand namespace modules were deleted and the resource classes lost their *Resource/*Namespace suffixes (PhoneNumbersResource -> PhoneNumbers, CallingNamespace -> Calling, ...). client.<ns>.<resource> usage was unaffected, but direct imports broke: from signalwire.signalwire.rest.namespaces.phone_numbers import PhoneNumbersResource from signalwire.signalwire.rest.call_handler import PhoneCallHandler Re-add the 20 namespace modules + call_handler.py as thin RE-EXPORT STUBS (old name -> generated bare name) so those imports keep working. Each carries the `x-sdk-back-compat-shim` marker so the cross-port surface oracle skips it — these are PYTHON-ONLY and must NOT be added to other ports. (client.compat is deliberately NOT shimmed — that API was intentionally removed. AutoMaterializedWebhook has no equivalent and is not re-exported.) 506 rest tests pass; ruff + mypy clean; the shims add ZERO oracle surface (verified byte-identical with/without). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: emit DeprecationWarning from the back-compat shim imports Each Python-only shim (the 20 namespace re-export stubs + call_handler.py) now warns on import that the path is deprecated, pointing at client.<ns> (the supported access). Old imports keep working; normal client.<ns>.<resource> usage emits NO warning (the client tree never imports the shims). Message is path-accurate — it recommends client.<ns> rather than naming a per-symbol module (FabricResource lives in _base, others in *_resources_generated; client.<ns> is correct for all). Shims stay oracle-invisible (x-sdk-back-compat-shim). 506 rest tests pass; ruff clean; client.* usage verified warning-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * core: generated SWAIG response-action layer (from the vendored mod_openai spec) swaig_actions_generated.py — 27 typed action builders + the <Action> value TypedDicts, generated from porting-sdk/swaig-specs/swaig-response.yaml (vendored from mod_openai). The ergonomic FunctionResult methods will compose this typed layer. ruff + mypy --strict clean; runtime-verified to build correct wire output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * core: type swaig_function.execute(raw_data) with the generated SwaigRequest swaig_request_generated.py — the SwaigRequest TypedDict (+ SwaigArgument), generated from porting-sdk/swaig-specs/swaig-request.yaml (vendored from mod_openai). swaig_function.execute now takes raw_data: "SwaigRequest | None" (TYPE_CHECKING import — no runtime cost/cycle; a plain dict at runtime), replacing the bare dict[str, Any]. The inbound function-webhook payload is now statically typed where the handler receives it. ruff + mypy --strict clean; 1815 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * core: type the post-prompt handler with the generated PostPrompt payload post_prompt_generated.py — the full post-prompt callback payload tree (PostPrompt envelope + the call_log role-union + swaig_log/times + nested sub-objects), generated from porting-sdk/swaig-specs/post-prompt.yaml (vendored from mod_openai). agent_base: - on_summary(summary: PostPromptData | None, raw_data: PostPrompt | None) — the user callback now receives a typed payload. - _find_summary_in_post_data(body: PostPrompt) — typed; the post_prompt_data.parsed/.raw reads type-check against the corrected extract_json shape. Behavior unchanged (annotations only; the legacy top-level `summary` fallback is preserved via an untyped probe). TYPE_CHECKING imports (no runtime cost/cycle). mypy --strict clean; 1815 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * test(rest): generated wire-test suite replaces the hand *_full_mock tests Add the 19 generated <ns>_generated_test.py files (422 tests: success + error per route, asserting method/matched_route against the mock) and delete the 20 superseded hand test_*_full_mock.py files. The generated suite covers every route the hand full-mock tests covered (178) plus 31 more, captured from the real client + spec operationIds. Behavioral hand tests (pagination, response parsing, client construction, the per-namespace test_<ns>.py) are KEPT — they assert things beyond wire shape. 613 REST tests pass; 5465 unit tests pass overall. Generated files are ruff + mypy --strict clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * test+types: mypy --strict across the WHOLE test suite (8950 -> 0) + tests in the gate Drove mypy --strict to zero across the entire tests/ tree (was ~8950 errors) under the REAL whole-tree config, and added `tests` to [tool.mypy] files so the TYPECHECK gate now covers tests too (a new untyped test fails CI). - generated REST tests are strict-clean by construction; hand tests fully annotated with REAL types (-> None, typed fixtures, typed handlers) — never blanket Any. - mock-monkeypatch sites (mock.method = ...) carry # type: ignore[method-assign] + reason; intentional invalid-input tests carry # type: ignore[arg-type] + reason; stale ignores from an earlier --ignore-missing-imports pass removed (warn_unused_ignores keeps them honest). - real fixes surfaced by the strict pass: union-attr None-narrowing (assert), override-sig alignment, ClassVar/tuple/None-callable, generic type-args. - conftest.py + the 3 top-level tests/*.py fully typed; AgentBase imported from its canonical module so mypy resolves it. Source fix: prefabs (survey/receptionist/faq_bot/concierge) on_summary overrides updated to the new PostPromptData/PostPrompt base signature — the post-prompt retype had made them incompatible overrides (the whole-tree run caught this; source TYPECHECK would have gone red). Verified: `mypy --config-file pyproject.toml` = 0 across 338 files; 5523 unit tests pass (the one bedrock-example failure is pre-existing — imports a nonexistent run_agent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * fix(swml): builder.ai must wrap prompt as object {text:} — bare string is a fatal call error The SWML `ai` verb's `prompt` must be an OBJECT — {"text": ...} or {"pom": [...]}. A bare string is a FATAL error in the AI engine: mod_openai app_config.c does `if (!cJSON_IsObject(prompt))` -> fires calling.error and aborts the call (verified against mod_infrastructure + mod_openai source, not just the SDK schema). SWMLBuilder.ai() emitted `config["prompt"] = prompt_text` (bare string) — and likewise post_prompt. Now wraps text -> {"text": ...}, pom -> {"pom": ...}, post_prompt -> {"text": ...}, matching the production AgentBase path (agent_base.py:1294) and the engine. Latent until now because the production render path builds the wrapped object itself and bypasses SWMLBuilder.ai; only direct builder / SwmlRenderer.render_swml(str) callers hit it. - update test_swml_builder assertions to the object form (they enshrined the bare-string bug via a mock service that skips schema validation). - render_swml tests now pass real `str`/`pom` args (11 of 12 # type: ignore[arg-type] dropped; the remaining one is a legit None invalid-input test) — proving str -> valid SWML end-to-end. Cross-port: flagged in porting-sdk GLOBAL_MEMORY (Go already fixed; .NET still has it; Contexts path is the OPPOSITE contract — bare string is correct there, do not "fix"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate with REST fully CLOSED (extras removed from create/update) Regenerated the REST resource modules — create/update/operation methods are now fully closed to the spec fields (no `extras` param). Reserved-word fields like `from` are exposed as the typed kwarg `from_` (mapped back to the wire key), so nothing is lost. - test_small_namespaces_mock: mfa.call now passes from_="..." instead of extras={"from":...}. mypy --strict clean (338 files); 5466 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate — reserved-word fields get the **_reserved_kw literal-key door Methods whose wire body has a reserved-word field (e.g. mfa.call's `from`) now expose both the typed `from_` param AND a `**_reserved_kw` tail, so callers can pass the literal wire key via `**{"from": ...}`. The `_reserved_kw` tail is dropped by the signature oracle (Python-only workaround; surface stays closed cross-port). mypy --strict clean; 613 REST tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * rest: regenerate — REST re-opened (extras + **kwargs door restored on create/update) create/update/operation methods carry the typed spec fields + extras + **kwargs again. The override-ignore is now emitted only where the signature genuinely narrows the base (required- param create / positional-body), so warn_unused_ignores stays satisfied. mypy --strict clean (338 files); 613 REST tests pass; 5466 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF * perf(ci): S1 fail-fast + S2 concurrent cheap-gate wave via shared DAG scheduler run-ci gates now run through porting-sdk/scripts/gate_scheduler.sh: pure-Python side-effect-free gates run CONCURRENTLY (S2), heavy gates (TEST/build) deferred behind the cheap wave (S1 fail-fast). Data-deps honored (DRIFT deps=SIGNATURES; surface-mutating gates share res=surface); only genuinely-contending gates serialized (java res=gradle) — schedule-freely, throttle only what actually breaks. Per-gate PASS/FAIL + FAILED_GATES tally preserved; opt-in --fail-fast (default = full report). Gate set byte-identical to before (no gate dropped/added). Measured warm: ts 37->20s, go 17->5s, rust 94->30s; injected cheap-gate failure --fail-fast fails in ~0-2s vs full serial run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * perf(ci): cache pip deps (setup-python cache: pip) — install was 87s/88% of the job The 'Install signalwire-python (editable + dev deps)' step (pip install -e . + requirements-dev) was ~87s, 88% of python's ~99s Test job — re-downloading + building the same wheels every run (the gates themselves are fast). Enable setup-python's pip cache keyed on pyproject.toml + requirements-dev.txt so warm runs install from cached wheels. setup-python manages the cache save/restore itself (no save-on-failure issue). A manifest change invalidates + rebuilds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * ci: warm-pip-cache run to measure the cache Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * Revert "perf(ci): cache pip deps (setup-python cache: pip) — install was 87s/88% of the job" This reverts commit d435615c2d4f9bb3db7513ed9a63cd9b186d62ec. * regen(rest): **_reserved_kw door in generated create/update (was **kwargs) Regenerated from porting-sdk's updated generator (4eb004d): every `extras`-open create/update/operation method now emits a `**_reserved_kw` var-keyword tail instead of a bare `**kwargs`. One door name everywhere `extras` is present; the runtime behavior is identical (both merge unknown/reserved-word fields into the wire body). The extracted oracle drops the tail by kind, so the cross-port surface reads by the typed params + `extras`. GEN-FRESH passes and the 613 REST tests are green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * ci: re-trigger against porting-sdk main (fix #58 merged) Empty commit to run #39's CI against the updated porting-sdk main (f8d45b0, PR #58) now that the **_reserved_kw door generator fix is merged there. GEN-FRESH / DRIFT / tests should now reproduce cleanly against the post-#58 generator + oracle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * fix(docs): remove orphaned Twilio-compat docs/examples; ignore 5 real-but-off-surface symbols doc-audit went red after the branch regenerated against the post-#53/#58 porting-sdk surface, surfacing 24 unresolved symbols in docs/examples. Root cause + fix: COMPAT (19 of 24): commit 1a75d18 removed the entire Twilio-compat REST API (namespace, generated types, tests) — "we should not generate anything for compat" — but left the docs + examples that referenced the now-deleted `client.compat.*` namespace (which no longer exists on RestClient). Deleted the orphans rather than allowlist a phantom surface (RULES.md: delete removed/aspirational surface, don't ignore it): - rest/docs/compat.md + rest/examples/rest_compat_laml.py (and the rest/rest/ dupes) - the compat rows/links in rest/README.md, rest/docs/client-reference.md, README.md REMAINING 5 → DOC_AUDIT_IGNORE.md with rationales (real symbols genuinely off the cross-port surface, matching the file's existing conventions): - error → logging.Logger.error (stdlib; sits beside debug/info/warning) - parse_args → argparse.ArgumentParser.parse_args (stdlib; beside parse_known_args) - build_index / build_index_from_sources / migrate_sqlite_to_pgvector → real methods in signalwire/search/ (Python-only search skill; already skip-listed in porting-sdk) doc-audit now passes (2886 resolved / 0 unresolved). The .py comment "LAML / Twilio-compat" on the real set_cxml_webhook method is a descriptive comment, not a phantom call — left. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * fix(test): make test_schema_utils import 3.10-compatible (was aborting all unit collection) tests/unit/utils/test_schema_utils.py:19 did `from importlib.resources.abc import Traversable`, but `importlib.resources.abc` doesn't exist until Python 3.11 — on 3.10 it raises ModuleNotFoundError. Because pytest collects the whole `tests/unit/` tree in one session, that single bad import aborted collection with "1 error during collection" and every test on ALL FOUR Python versions was reported as failed (0 actually ran). `Traversable` is used only as a type annotation on a local test helper. Guarded the import: try `importlib.resources.abc` (3.11+), fall back to `importlib.abc.Traversable` (the canonical pre-3.11 location, present since 3.9). Pre-existing since 6510453 (the mypy-strict pass); unrelated to the type-generation work on this branch but it's what was reddening the TEST gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * fix(ci): green the TYPECHECK, FMT/LINT, and REST-COVERAGE gates Three pre-existing red gates on the branch (unrelated to the type-generation work but reddening the TEST job), now fixed and verified with a full local run-ci (CI PASS): TYPECHECK (mypy): agent_base.py set self._multilingual = None while mypy inferred dict[str, Any] for the attr from set_multilingual()'s assignment → conflict. Declared it explicitly on the base AIConfigMixin as `dict[str, Any] | None = None` (the honest type: None at init, dict once configured); agent_base's init is a plain `= None`. FMT/LINT (ruff): 8 hand-written source files were unformatted (ruff format applied). The reformat wrapped three namespace re-export imports across lines, which moved their `# noqa: E402` from the `from ... import (` line to the closing `)` — E402 then fired on the unmarked opening line and RUF100 flagged the now-"unused" noqa on the close. Moved each noqa back onto the import-start line. ruff format + check both clean. REST-COVERAGE: the gate drove the wire suite with `-k full_mock`, but the generated wire tests (bb2c6cf) replaced the hand `*_full_mock` tests and use `*Wire` classes, so the selector matched 0 tests → empty journal → coverage failed. Switched to `-k Wire` (422 wire tests, 209/228 routes covered, parity clean). Also removed the now-stale `compatibility.list_available_phone_number_resources_by_country` allowlist entry in REST_COVERAGE_GAPS.md (the compat namespace was removed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * feat(web): name the as_router return type (HostAppRouter) for cross-port parity as_router() (embed this agent/service's routes into a host web app — `app.include_router(agent.as_router())`) returned a bare FastAPI APIRouter, a framework-specific type that couldn't reconcile with the ports' native mountable types, so the signature oracle HID the method and the ports either suppressed it via omissions or filed type-shaped `impossible:` tags. Introduce `signalwire.core.web.HostAppRouter`, a behaviour-neutral subclass of `fastapi.APIRouter` (adds no state/methods — at runtime it IS an APIRouter, so `include_router`, `isinstance`, and route introspection all work unchanged; verified: 101 web-layer tests pass, mypy --strict clean). Both `SWMLService.as_router` and `WebMixin.as_router` now return `HostAppRouter`. This gives the capability a stable, named cross-port type each port mirrors in its own idiom (go http.Handler, .NET RequestDelegate, ruby Rack app, …) via the type-alias table — enforcing the capability while absorbing the framework type as idiom. Zero runtime behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * feat(security): decomposed cross-port webhook validate() core Extract the framework-free webhook-validation decision from the FastAPI dependency into a portable `validate(method, url, headers, body, *, signing_key) -> Optional[(status, headers, body)]` function — the same shape every port already expresses (dotnet WebhookValidationMiddleware.Validate, Rack/PSGI middleware whose response IS a (status,headers,body) triple, a Hono handler, ...). This names the cross-port capability so the signature oracle can require it and the gate can enforce the webhooks.md contract, instead of it being hidden behind per-port `impossible: returns a FastAPI Depends` tags. Behaviour-neutral: make_webhook_validation_dependency now decomposes the FastAPI Request into (method, url, headers, body) and delegates to validate(); the FastAPI-specific parts (raw-body capture, proxy-aware URL reconstruction) stay as the idiom wrapper on top. The inlined _extract_signature_header helper is folded into validate(). 36 webhook tests pass, mypy --strict clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * feat(core): decomposed framework-free handle_request dispatch core Expose the framework-free request-dispatch capability the ports already ship (e.g. dotnet `(int,Dictionary,string) HandleRequest(method,path, headers,body)`) as a public python method, with get_app/serverless left as idiom adapters on top. Behavior-neutral: the FastAPI path produces identical responses (same 401-auth and 307-redirect paths, same 200 SWML). - Decompose the routing callback to the port-shared (body, headers) shape: register_routing_callback's callback_fn is now Callable[[dict, dict], str | None] in SWMLService + WebMixin; call sites pass (body, dict(request.headers)); sip_routing_callback (agent_base) and the AgentServer SIP/global callbacks adopt the same signature (all only read body, never the request). - Add public SWMLService.handle_request(method, url, headers, body=None) -> tuple[int, dict[str, str], str]: the framework-free dispatch core (proxy detection, basic-auth, routing-callback check, on_request modifications) over primitives, backed by a shared _handle_request_core. _check_basic_auth and _detect_proxy_from_request are decomposed to primitive helpers (_check_basic_auth_headers / _detect_proxy_from_primitives) that the FastAPI-typed versions delegate to. - Refactor SWMLService._handle_request (FastAPI) to a thin adapter that extracts primitives, delegates to the core, and marshals the (status, headers, body) triple back into a FastAPI Response — preserving the original HTTPException-based 401 and the 307 redirect exactly. - AgentBase overrides handle_request to render via _render_swml (mirroring its _handle_root_request path) instead of the base render_document. Tests prove the decomposed core matches the FastAPI path across all three behavior paths: 401 (missing/bad auth), 307 (routing-callback redirect), 200 (plain + agent SWML render), for both SWMLService and AgentBase. Verify: mypy --strict clean (whole tree, 339 files); full unit suite 5478 passed / 99 skipped (pre-existing broken tests/integration/relay/ test_relay_live.py deselected); ruff format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * chore(lint): sort webhook __all__ (RUF022) + drop dead APIRouter import (F401) Both were pre-existing lint debt flagged during Pass 3: RUF022 from the Pass-1 webhook validate __all__ addition, F401 from as_router leaving APIRouter used only in a docstring. Behavior-neutral. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * ci: re-run against updated porting-sdk oracle (callback_fn decompose) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * types: ship py.typed (PEP 561) so consumers' type-checkers read the SDK's inline types The source tree is already mypy --strict clean, but without a py.typed marker type-checkers treat the installed package as untyped (Any everywhere), so downstream `mypy my_agent.py` catches nothing against the SDK. Add the marker in the import-package root + list it in setuptools package-data. Verified it lands in the built wheel (signalwire/py.typed present in signalwire_sdk-3.0.2-py3-none-any.whl). No code change; pairs with the generated TypedDicts this branch adds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * docs: burn SNIPPET-COMPILE backlog (task #93) — 225 python snippets green The SNIPPET-COMPILE gate py_compiles every ```python fenced block in tracked *.md (as-is, no wrapping). 225 blocks failed. Resolved all of them: - 4 fixed (real syntax rot): docs/contexts_guide.md had complete class X(AgentBase) examples with a `\` line-continuation placed AFTER a trailing `# comment` in fluent chains — the comment swallows the backslash, so the next `.method(...)` lands at an unexpected indent. Moved the inline comments off the continuation. API names verified against signalwire/signalwire/core/contexts.py — no API changed. - 221 suppressed (genuine illustration fragments) via `<!-- snippet: no-compile <reason> -->`: top-level `await call.foo(...)` call illustrations (await-fragment), pseudo-signatures like `foo(x: str) -> T` (signature-illustration), indented class/method-body excerpts, `def foo(): ...` signature-only, config/JSON excerpts, and example-output blocks. Each classified individually against the real SDK source; no blanket-suppression of real code. Gate: snippet_compile.py --port python --report-only -> 0 failures (641 compiled, 221 suppressed). Split by directory (canonical + identical relay/relay & rest/rest dup trees): relay/docs (+dup) 54 | docs/ 88 | tutorial/ 10 signalwire/skills 7 | signalwire_ai_blog 5 | rest/docs (+dup) 1 one_sdk.md 1 | contexts_guide fixes 4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * rest(fabric): regenerate generated surface for optional server-assigned identity Completes the already-approved f53e7bb spec fix (fabric create requests must not require server-assigned identity). Regenerated fabric_resources_generated.py so AiAgents.create's agent_id and SipEndpoints.create's id are now `uuid | None = None` (moved to the end of the keyword params, after the required fields) instead of required, matching the fabric openapi. The regenerator also rewrote tests/unit/rest/fabric_generated_test.py to drop the now-optional agent_id / id arguments from the create wire tests. GEN-FRESH now green; no unrelated API drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * test: Tier-0 regression pins — wire percent-encoding + User-Agent version Two Layer-D regression pins co-located with the generated REST wire suite, driving real requests through the shared mock_signalwire and asserting the journal (the same journal REST-COVERAGE reads). - Percent-encoding pin: list(q="a b&c+dé", tag="x=y", emoji="smile☃") → the journal's decoded query_params must equal the exact input. Pins the rust no-percent-encoding bug class; a no-encoding client mangles it. GREEN on python. - User-Agent pin: UA version segment must equal the installed package version. Marked xfail(strict=True) because it catches a LIVE python bug — rest/_base.py hardcodes signalwire-agents-python-rest/1.0 (!= installed 3.0.2). Stays green now, flips to a hard failure the instant the UA is derived from pkg version. wait()-liveness pin was NOT authored — the WIRE-RELAY differ is shape-only with no deadline concept and its recording client short-circuits the socket pump; a real liveness harness (mock_relay WS + deferred event + deadline) is needed first. Behavioral-envelope pins need an envelope spec + mock delay/429/503 endpoint. Both tracked as follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * fix(rest,examples): derive REST User-Agent from package version; fix broken SWMLService examples Two approved python-reference fixes surfaced by the 2026-07-06 DX review and the php96/ruby95 SWML-service example triage. 1. REST User-Agent (SDK_BUG_LEDGER P1, real wire bug): rest/_base.py hardcoded `User-Agent: signalwire-agents-python-rest/1.0` while the package is at 3.0.2 -- wrong product token AND a stale version. Now derived at runtime from importlib.metadata.version("signalwire-sdk") as `signalwire-python/<version>` (before -> after: signalwire-agents-python-rest/1.0 -> signalwire-python/3.0.2). The wire_regression_pins UA test had a strict xfail pinning the bug; the xfail marker is removed so the pin now HARD-ENFORCES the fix and guards against re-drift. 2. SWMLService examples called AgentBase-only surface. basic_swml_service.py, auto_vivified_example.py, and dynamic_swml_service.py subclass SWMLService but called `self.add_answer_verb()` (AgentBase-only) and `service.run(...)` (does not exist), so every one crashed with AttributeError on construction / on start. Fixed to the real SWMLService API: `self.add_verb("answer", {})` and `service.serve(host=, port=)`. All service classes now render valid SWML and the dynamic on_request path exercises cleanly. run-ci.sh: all 10 gates PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * rest/docs: burn public_jargon to 0 + gate publish workflows on CI public_jargon (50 -> 0): reword the 20 back-compat shim namespace docstrings and call_handler.py to user-facing deprecation notices (drop the x-sdk-back-compat-shim / "surface oracle skips this" internal jargon), and reword hand-written docstrings in _base.py, _mixin_host.py, web.py, webhook_validator.py, webhook_middleware.py, agent_base.py to describe the API for a user (drop porting-sdk/oracle/parity/"the TS port" references). No wire or API behavior changed - docstrings only. release_fresh (RED -> gated): both publish-dev.yml and publish-release.yml uploaded to PyPI with no tests run first. Add a `test` job mirroring test.yml (porting-sdk checkout + `bash scripts/run-ci.sh` across py3.10-3.13) and make each publish job `needs: test`, so a red tree can no longer ship. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * ci: wire 5 expansion gates into run-ci.sh as blocking (enforcing) Wire GEN-TYPE-DEGENERACY, PUBLIC-JARGON, ROUTE-COLLISION, GEN-IDIOM, and RELEASE-FRESH into scripts/run-ci.sh as blocking (non-report-only) gates, modelled on the Day-one gates' sched_gate idiom. python is the reference: GEN-TYPE-DEGENERACY and GEN-IDIOM self-skip clean; PUBLIC-JARGON is clean; RELEASE-FRESH confirms both publish workflows gate before publishing (fixed in a31611d). ROUTE-COLLISION enforces against the port's route-registry. ROUTE-COLLISION flagged the two list_addresses singular-sub-path route-splits (CallFlows / ConferenceRooms). These are a spec-declared platform quirk (rest-apis/fabric/openapi.yaml:801,1074): addresses live under the SINGULAR call_flow / conference_room sub-path while the collection is plural. Python overrides list_addresses to the singular path (fabric_resources_generated.py :327,:477), so there is exactly ONE live route -- the canonical spec path. Add ROUTE_COLLISION_ALLOW.md carrying the two entries, mirroring the identical user-approved 2026-07-07 exceptions in go/java/cpp (of which Python is the reference origin). Gate exits 0 [allow]. bash scripts/run-ci.sh: PASS with all 5 new gates green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * feat(swaig-test): add canonical --parse-only / --dry-run flag (#97) Add a --parse-only flag (alias --dry-run) to the swaig-test CLI that validates an invocation's arguments and exits WITHOUT loading the agent, touching the filesystem, or making any network request. Valid args print exactly "parse OK" and exit 0; invalid args (unknown flag, missing required positional, mutually-exclusive flags) exit non-zero via argparse. This is the reference form the other SDK ports mirror so the cross-port DOC-CLI gate can validate documented swaig-test invocations exactly instead of heuristically (run + classify parser-rejection markers). Details: - Detected early and stripped from argv so it is position-independent — honored whether it precedes or follows --exec (which otherwise consumes every trailing token as a function argument; the gate appends the flag). - Bypasses --dump-swml's global stdout suppression so the "parse OK" line is always emitted. - Documented as a canonical contract in docs/cli_guide.md. - 7 unit tests covering valid/alias/no-file/after-exec and the three invalid-arg rejection paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * docs(readme): include quickstart code blocks from gate-compiled fixtures Convert the three README quickstart code blocks (AI agent, RELAY client, REST client) to README-INCLUDE markers pulling from real, compilable example fixtures under examples/, so the doc code is byte-identical to working code and can't rot. - examples/quickstart_agent.py (region: agent) - examples/quickstart_relay.py (region: relay) - examples/quickstart_rest.py (region: rest) readme_include gate: 3 sites verified clean. run-ci.sh: PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj * ci: wire README-INCLUDE gate into run-ci (enforcing) (#108) 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 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Coordinated-With: porting-sdk@wave6/ctor-dunder-fold
What this is
The three ChatGateway commits (
8b790ea,cac3118,20663a6) landed onmainwithout a full
run-ci, reddening four gates. This fixes each at its source.It does not touch the gates that are wave6 coordination artifacts — see
"Not in scope" below.
Fixed
LINT —
RUF022(__all__unsorted inai_chat/__init__.py) andSIM102(nested
ifinsearch/document_processor.py).FMT —
ruff formatoverai_chat/gateway.pyandcore/function_result.py.Applied with the pinned
ruff==0.15.21; a newer ruff reformats 19 filesinstead of 2, which is exactly what the pin in
requirements-dev.txtexists toprevent.
TYPECHECK — two root causes, not 105 problems:
tests/unit/ai_chat/test_gateway.pyshipped fully unannotated (44no-untyped-def+ 19no-untyped-call). The mypy config putstestsinscope deliberately — "a new untyped test fails the gate" — so it is annotated
throughout.
FunctionResult.responsewidened tostr | dict[str, Any], so 37 sites doing.response.lower()stopped type-checking. Each getsassert isinstance(<r>.response, str)beside the existingassert isinstance(<r>, FunctionResult): a real assertion that narrows theunion and pins the contract, not a
castpapering over it.FunctionResult.hold:boolsubclassesint, so excluding bools from theback-compat int-swap left
str | bool. Handled explicitly.hold(120)stillmeans
hold(timeout=120)— the existing back-compat shim is untouched.ChatGateway.visible_messages/last_activityacceptNoneand non-dictitems by design (
for msg in messages or [],if not isinstance(msg, dict): continue) and are tested for it, but weretyped
list[dict[str, Any]]. Widened to match the real contract. Free tochange:
ChatGatewayis new surface no port has implemented yet.NO-CHEAT — three origin tests asserted nothing at all, so they passed
regardless of whether the code worked. Each now pairs the allowed case with the
refusal that proves it is a deliberate exemption and not open-by-default:
localhost vs an unlisted origin, a listed origin vs a lookalike domain, and
absent vs present-but-unlisted.
Verification
LINT,FMT,NO-CHEATpass; 29 other gates pass.mypyclean over all 8 files CI reports.mcp_gatewayfailures are pre-existing — verified bystashing this branch's changes and running them on unmodified
main, wherethey fail identically. They are environment-dependent and CI passes them.
Not in scope (deliberately)
GEN-FRESH,DRIFT,SEMVER-DIFFand DOC-AUDIT's unresolvedrouterarecoordinated-pin artifacts, not defects in this code.
PORTING_SDK_REFisset to
wave6/ctor-dunder-fold, so CI builds against that branch — which wascut before the ChatGateway commits and therefore has no
ChatGatewayin itsoracle. Regenerating the oracle standalone is not possible: doing it on top of
wave6 produces an 877+/544- diff whose deletions are wave6-only surface that
maindoes not have.That half is already PR #78's ("wave6: the reference half of the coordinated
pass"), which regenerates those exact generated files and whose
testjobs passon 3.10-3.13. Those gates resolve when the wave lands with
main's commitsabsorbed.
Worth noting separately:
mypyis unpinned (mypy>=1.8) inrequirements-dev.txtwhileruffis pinned exact. It happened to resolve to2.3.0 both locally and in CI, but that is luck — same local-vs-CI drift class
the ruff pin was added to stop.
🤖 Generated with Claude Code
https://claude.ai/code/session_015dYktt85Ltj3oK9gG5VBww