Skip to content

fix(ci): green the gates the ai_chat/ChatGateway commits reddened - #81

Merged
anthmFS merged 2 commits into
mainfrom
fix/gateway-gates-green
Aug 10, 2026
Merged

fix(ci): green the gates the ai_chat/ChatGateway commits reddened#81
anthmFS merged 2 commits into
mainfrom
fix/gateway-gates-green

Conversation

@anthmFS

@anthmFS anthmFS commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Coordinated-With: porting-sdk@wave6/ctor-dunder-fold

What this is

The three ChatGateway commits (8b790ea, cac3118, 20663a6) landed on main
without 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

gate before after
LINT FAIL PASS
FMT FAIL PASS
NO-CHEAT FAIL PASS
TYPECHECK 105 errors clean on every file CI reports

LINTRUF022 (__all__ unsorted in ai_chat/__init__.py) and SIM102
(nested if in search/document_processor.py).

FMTruff format over ai_chat/gateway.py and core/function_result.py.
Applied with the pinned ruff==0.15.21; a newer ruff reformats 19 files
instead of 2, which is exactly what the pin in requirements-dev.txt exists to
prevent.

TYPECHECK — two root causes, not 105 problems:

  • tests/unit/ai_chat/test_gateway.py shipped fully unannotated (44
    no-untyped-def + 19 no-untyped-call). The mypy config puts tests in
    scope deliberately — "a new untyped test fails the gate" — so it is annotated
    throughout.
  • FunctionResult.response widened to str | dict[str, Any], so 37 sites doing
    .response.lower() stopped type-checking. Each gets
    assert isinstance(<r>.response, str) beside the existing
    assert isinstance(<r>, FunctionResult): a real assertion that narrows the
    union and pins the contract, not a cast papering over it.
  • FunctionResult.hold: bool subclasses int, so excluding bools from the
    back-compat int-swap left str | bool. Handled explicitly. hold(120) still
    means hold(timeout=120) — the existing back-compat shim is untouched.
  • 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 contract. Free to
    change: ChatGateway is 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-CHEAT pass; 29 other gates pass.
  • mypy clean over all 8 files CI reports.
  • Full unit suite: 5940 passed.
  • The 6 remaining mcp_gateway failures are pre-existing — verified by
    stashing this branch's changes and running them on unmodified main, where
    they fail identically. They are environment-dependent and CI passes them.

Not in scope (deliberately)

GEN-FRESH, DRIFT, SEMVER-DIFF and DOC-AUDIT's unresolved router are
coordinated-pin artifacts, not defects in this code. PORTING_SDK_REF is
set to wave6/ctor-dunder-fold, so CI builds against that branch — which was
cut before the ChatGateway commits and therefore has no ChatGateway in its
oracle. 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
main does 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 test jobs pass
on 3.10-3.13. Those gates resolve when the wave lands with main's commits
absorbed.

Worth noting separately: mypy is unpinned (mypy>=1.8) in
requirements-dev.txt while ruff is pinned exact. It happened to resolve to
2.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

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).
@anthmFS
anthmFS merged commit f377645 into main Aug 10, 2026
0 of 5 checks passed
@anthmFS
anthmFS deleted the fix/gateway-gates-green branch August 10, 2026 18:34
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant