Conversation
anthmFS
added a commit
that referenced
this pull request
Aug 10, 2026
* fix(ci): green the gates the ai_chat/ChatGateway commits reddened 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. * fix(gen-fresh): regenerate generated types against the pinned wave6 specs 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
force-pushed
the
wave6/ctor-dunder-fold
branch
from
August 12, 2026 19:12
96d60a2 to
13760c3
Compare
mjerris
force-pushed
the
wave6/ctor-dunder-fold
branch
from
September 23, 2026 23:03
3931666 to
554af5f
Compare
…gnal handlers, skills paths Closes the 15 Windows TEST failures left by PR #76 (which took the count 36 -> 15), measured from Multi-OS run 30261304144 / windows-latest. One of the three defect classes is a PRODUCT bug, not a test bug. 1. PRODUCT — RelayClient.run() was broken for every Windows user (7 tests) _run_forever() called loop.add_signal_handler(SIGINT, ...) unguarded on its FIRST statement. Loop-level signal handling is a Unix-only asyncio capability: both Windows event loops raise NotImplementedError unconditionally, so the exception escaped before connect() was ever reached — RelayClient.run() could not establish a RELAY connection on Windows at all. Guarded with a NotImplementedError fallback that degrades to KeyboardInterrupt-driven shutdown (Ctrl+C still stops the client; only the graceful _shutdown() handshake is lost). Also replaced the __import__("signal") inline with a module-level import. Covered by a new platform-independent regression test that forces the exact Windows condition (patching the loop method to raise what Windows raises) rather than skipping on win32, so the contract is exercised on every OS. Verified to FAIL against the unfixed product on macOS with the same NotImplementedError at client.py:684 as the Windows traceback. The 7th relay failure was a separate mechanism: the ping-loop test patched _EXECUTE_TIMEOUT=0.01 around client.connect(), putting the auth round-trip under a 10ms deadline. The handshake needs several event-loop turns, and 10ms is at/below the Windows asyncio timer granularity (~15.6ms clock tick), so connect() could time out before the loop ran the recv task. Scoped the patch to the pings the test is actually about. A deadline sweep on POSIX reproduces the identical "Request timeout for signalwire.connect" error deterministically (20/20) once the deadline drops below the turns required. 2. TESTS — POSIX-separator expectations (3 tests) test_schema_utils (2) and test_registry (1) compared product output to hardcoded POSIX literals. The product is separator-correct in all three cases: it returns str(Path(...)), composes with os.path.join(), and splits on os.pathsep. Build the expectation the same way the product builds the value (compare Path objects / computed joins) instead of a POSIX-only spelling. Confirmed under Windows path semantics (ntpath/PureWindowsPath) on POSIX: the old literals match only on POSIX, the new expectations match on both — so Windows keeps real coverage rather than losing it to a skip. 3. TESTS — claude_skills paths (5 tests) - WinError 267 ("directory name is invalid"): the test passed Path("/tmp") as the subprocess cwd. /tmp does not exist on Windows, so the spawn failed before the timeout could fire. Switched to pytest tmp_path. The command was also `sleep 10`, which is not a Windows shell builtin and exits immediately; replaced with a portable python -c sleep so the timeout path is genuinely exercised (test now takes ~1.01s, i.e. it really blocks). - 8.3 short-path mismatches (3 tests): setup() canonicalizes skills_path via .resolve(), which expands the Windows 8.3 temp dir to its long form (RUNNER~1 -> runneradmin). The tests compared an unresolved tempfile path to resolved product output. Resolve both sides. - Removed all 15 hardcoded /tmp uses from this file (a standing project rule bans /tmp outright); they were inert placeholders except where noted above. No test was blanket-skipped: every disposition is either a product fix or an expectation corrected to match platform-correct product behavior. Verification: bash scripts/run-ci.sh exit 0, all gates PASS (TEST, FMT, LINT, TYPECHECK, DRIFT, SPEC-PARITY, REST-COVERAGE, ...); tests/unit 5710 passed, 3 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
`strip_control_chars` never used its `logger` or `method_name` parameters — they existed only to satisfy structlog's `(logger, method_name, event_dict)` processor calling convention. That made the recorded public contract describe structlog plumbing rather than what the function does, and asked every port to carry two permanently-dead parameters. The public function is now `strip_control_chars(event_dict)` — one parameter, the thing it actually uses. A private `_as_processor` adapter supplies the structlog protocol at the two registration sites (the structlog chain and the ProcessorFormatter chain). The adapter is private, so it adds no port surface. Adapters wrapping the same transform compare equal, which lets a test assert chain membership. Behaviour is unchanged: control characters are still stripped from log event values at both registration sites. Tests (tests/unit/core/test_logging_config.py::TestStripControlChars) exercise the REAL configured chain rather than the function in isolation, and were verified to fail against two deliberately broken adapters — a no-op adapter body, and dropping the adapter from only the ProcessorFormatter site (which the end-to-end output assertion alone cannot see, since the structlog chain strips first). `_drop_internal_keys` has the same 3-parameter shape but is private and therefore not port surface; it is deliberately left unchanged.
The multi-OS PACKAGE-SMOKE step invoked bare `python3`, which on the macOS runner
does NOT resolve to the interpreter actions/setup-python provisioned. Nightly run
30238061313, job macos-latest:
[FAIL] build: exit 1
/Library/Frameworks/Python.framework/Versions/3.12/bin/python3:
No module named build
Mechanism. `setup-python@v6` with python-version "3.12" provisions
/Users/runner/hostedtoolcache/Python/3.12.10/arm64 (the step's own log confirms
pythonLocation), and the earlier `pip install` step installed `build` THERE. But
the macOS image ships a pre-installed framework Python whose bin dir sits ahead of
the toolcache for the name `python3`, so bare `python3` selected
/Library/Frameworks/Python.framework/Versions/3.12/bin/python3 — a DIFFERENT
interpreter, without `build`.
The same run is the proof: the TEST step immediately above uses `python -m pytest`
(the setup-python shim) and passed, as did every `pip install`. Only the one step
spelled `python3` missed.
package_smoke.py is NOT at fault — it correctly uses sys.executable, so it
faithfully used whichever interpreter this line handed it, and it reported that
path verbatim in the error. Fixed by invoking `python` (setup-python's shim).
NOT fixed by `pip install build`: installing into the wrong interpreter would mask
the resolution bug rather than repair it, and would leave the step running an
interpreter nobody selected.
Sweep of the sibling workflows that call setup-python: doc-audit.yml had the only
other bare-`python3` `run:` lines (2). They are ubuntu-only, where setup-python
front-loads its dir so both names currently resolve to the toolcache — latent, not
live — but switched to the shim for consistency so the trap cannot activate if that
job ever gains a macOS runner. nightly.yml and live-smoke.yml call setup-python but
contain no bare `python3`, and are ubuntu-only.
Verified: `grep -n '\bpython3\b' .github/workflows/*.yml` now matches only
explanatory comments, no executable line. actionlint on both changed files reports
the same 6 pre-existing shellcheck info/style findings as before the change (all at
doc-audit.yml's untouched Summary step) — no new findings.
Known-latent, deliberately NOT changed here (needs an owner call): scripts/run-ci.sh
uses bare `python3` in 51 places. Both workflows that invoke it are ubuntu-only, and
that script also runs on developer machines where `python3` is the correct name and
`python` may not exist — so a blanket rename is a behavior change beyond this fix.
Corrects my own previous commit on this branch, which mis-attributed the failure to
interpreter resolution. Dispatching the workflow disproved that fix: with `python`
instead of `python3` the macOS job failed IDENTICALLY, one word different —
/Library/Frameworks/Python.framework/Versions/3.12/bin/python: No module named build
i.e. the SAME framework interpreter, reached via the other name. So on the
macOS/arm64 runner BOTH names resolve to the pre-installed framework Python;
setup-python does not win PATH there at all.
The real root cause, from the same log: `pip` is that framework Python's pip —
every dependency reports installing to
`/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages`.
The job is therefore CONSISTENTLY one interpreter, and there was never a mismatch
to fix. `build` is simply not installed, because **it is not a declared dependency
anywhere** — not in requirements-dev.txt, not in requirements.txt, not in
pyproject.toml. The gate relied on the runner image happening to ship it.
AGENT_RULES §7: a tool a gate needs is DECLARED, not assumed present. So:
requirements-dev.txt gains `build>=1.0.0`, and the step above already installs that
file. This is not "pip install build into the wrong interpreter" (which the brief
rightly forbids as masking a resolution bug) — there is no wrong interpreter here,
and declaring a real, undeclared dev dependency is the fix rather than the mask.
Note this gate has never passed for python on any OS: multi-os.yml is the ONLY
place PACKAGE-SMOKE runs for this port (nightly.yml and scripts/run-ci.sh do not
invoke it), so the "ubuntu ships build" path was never exercised either.
`python` (not `python3`) is KEPT, on its own merits: it is the same name `pip`
above pairs with, so the step provably runs the interpreter the deps went into, and
it stays correct if the image's PATH precedence ever changes. The workflow comment
is rewritten to state this true mechanism instead of the interpreter-mismatch story.
The doc-audit.yml python3→python change from the previous commit also stands — that
job is ubuntu-only and green either way; the shim is the consistent choice.
Windows remains red on this workflow for an unrelated, separately-assigned reason
(RED 3: four test-portability defects in the TEST step — WinError 32 on an unclosed
sqlite temp file, a hardcoded "/tmp/custom" assertion, a POSIX 0o755 mode check, and
cp1252 UnicodeEncodeErrors). That step fails before PACKAGE-SMOKE runs, so the
Windows job cannot confirm this fix either way.
…rmission bits, UTF-8 encoding
Four distinct cross-platform defects, all measured from nightly Multi-OS run
30238061313 (job windows-latest, step TEST: 36 failed / 2 errors / 5671 passed).
Two turned out to be product bugs, not test bugs.
1. sqlite handle outlives the temp file (PermissionError [WinError 32])
PRODUCT BUG. search/index_builder.py validate_index() closed its connection
only on the success path; the "Missing tables" early return and the except
both leaked it. Windows refuses to delete a file with a live handle, so the
fixture teardown's os.remove() raised. Fixed with contextlib.closing (NOT
`with sqlite3.connect(...)` — a Connection context manager commits but does
not close). Audited all 14 sqlite3.connect sites in search/ and found two
more unguarded on their error paths: migration.get_index_info() and
search_service._get_model_name(). Also fixed the test-side leak in
test_search_engine.py, where NamedTemporaryFile(delete=False) held its own
handle open while os.unlink ran inside the `with` block.
2. Hardcoded POSIX path assertion
str(Path) renders with the platform separator, so `== "/tmp/custom"` can
never hold on Windows (it yields "\tmp\custom"). Now compares Path objects.
Also removes the /tmp usage itself per the project rule (tmp_path instead).
Same fix in test_init_project.py::test_main_custom_dir, which failed the
same way via `'/tmp/custom' in 'D:\tmp\custom\testproject'`.
3. POSIX permission bits
Windows has no execute bit, so st_mode never carries 0o755. The two
observable-mode assertions are skipif(win32) with the reason recorded, and
the contract they were checking is now ALSO asserted platform-independently
(that chmod 0o755 is requested, and only for deploy.sh). POSIX coverage is
unchanged — nothing was deleted or weakened.
4. UnicodeEncodeError: 'charmap'
PRODUCT BUG. cli/dokku.py:1964 wrote generated files with write_text() and
no encoding, i.e. the platform default (cp1252 on Windows). The templates
embed box-drawing rules (U+2500/U+2550, 59-char runs — matching the log's
"position 130-188") and arrows, which cp1252 cannot represent. This caused 8
direct failures plus 4 more surfacing as generate() returning False
("Failed to generate project: 'charmap' codec can't encode..."). Fixed at
_write_file plus the two app.json reads. cli/init_project.py had the same
latent defect — 98 cp1252-hostile characters across 29 unguarded write_text
sites — fixed there too before it bites.
Proven on POSIX (not merely "looks right"):
- Defect 1: 3 new tests assert the platform-independent invariant (every
connection opened is closed, via a connect spy). Verified they FAIL against
the unfixed product code and pass with it.
- Defect 4: reproduced the Windows failure class on macOS with
`LC_ALL=C python -X utf8=0`, which yields
"'ascii' codec can't encode characters in position 2-80" — the same position
range as CI's charmap error. The 3 new UTF-8 round-trip tests fail without
the fix; with it, all 210 cli tests pass even under that hostile locale.
- Defects 2 and 3 are structural (Path comparison / platform skip) and remain
pending real confirmation on the Windows runner.
Full local gate run: `bash scripts/run-ci.sh` → CI PASS, exit 0 (37 gates).
Unit suite 5621 passed / 100 skipped on macOS.
Note: the Windows TEST step failing is what kept PACKAGE-SMOKE from running at
all on that job (TEST: failure -> PACKAGE-SMOKE: skipped). If TEST now passes,
PACKAGE-SMOKE will execute on Windows for the first time and may surface
unrelated failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
The new test_deploy_script_round_trips_as_utf8 asserted
read_bytes().decode('utf-8') == read_text(encoding='utf-8'). Those two views
legitimately differ on Windows: text-mode writes translate \n -> \r\n and
read_text translates it back (universal newlines), so the assertion compared
raw CRLF against normalized LF and failed on the runner -- while the encoding
fix it was guarding was working correctly (every U+2550/U+2192/U+2705/U+1F310
round-tripped intact).
Line endings are not what the test is about. It now compares against the source
DEPLOY_SCRIPT_TEMPLATE line-by-line and asserts the non-ASCII character set
survives byte-for-byte, which is the actual regression being guarded. Still
verified to fail against the unfixed product code (all 3 tests in the class fail
under LC_ALL=C -X utf8=0 without the encoding fix).
Caught by Multi-OS run 30260346853 (windows-latest), which this branch
dispatched -- Windows TEST went 36 failed -> 16 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…uite
Four checks porting-sdk DEFINES for python and this script never scheduled, so they had never run
against the reference. TLS-VERIFY and CA-VAR are SECURITY properties; the reference should not be
the one port where they go unchecked.
BURNED TO ZERO BEFORE WIRING, per the standing rule. Each run standalone against the current tree
first — all four already PASS, so this adds coverage without adding a red:
python CA-VAR PASS
python TLS-VERIFY PASS
python SECRET-SCRUB PASS
python LEDGER PASS (SUPPRESSION-LEDGER + IGNORE-LEDGER-VERIFY, 2 rules)
TIER: BLOCKING (per-PR), measured not assumed — 0s, 0s, 0s and 2s respectively. Nothing here
approaches the cost that would justify the nightly tier, where a regression sits unseen until the
next scheduled run.
A CORRECTION TO THE AUDIT THAT FOUND THIS: the fleet gate-wiring audit listed SUPPRESSION-LEDGER
among python's missing BEHAVIOURAL rules. It is not one — `behavioral.py --rules SUPPRESSION-LEDGER`
errors with "unknown rule id(s)" and lists the 14 rules python actually knows. SUPPRESSION-LEDGER
belongs to the LEDGER suite (suites/ledger.py), which python was not scheduling at all. Every other
port already schedules it; python was the only gap. Wired as the suite rather than as a lone rule,
matching how the other nine do it.
python schedules gates individually rather than through a BEHAVIORAL suite line, so these are four
new sched_gate entries rather than additions to a --rules list.
Verification: each gate run exactly as run-ci.sh now invokes it (same script, same --port/--repo,
same --rules), all PASS. `bash -n scripts/run-ci.sh` clean.
Found by the fleet-wide gate-wiring audit (task #55); completes that item's tier-1 across
perl 0db0b55, cpp 54abe7f, ruby dde0516, java 63acebd and this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
… is refused OWNER-RULED TWICE (2026-07-27 as task #56 option (b), re-confirmed 2026-07-29): a tool registered `secure=True` MUST REQUIRE a `__token`. Accepting the call when the token is ABSENT is a security defect, not the contract. A flag named `secure` that permits unauthenticated calls is a trap. THE DEFECT. core/agent_base.py:1413-1415 read: # Validate security token if present. token = request.query_params.get("__token") or request.query_params.get("token") if token: The ENTIRE validation block sat inside that conditional, so omitting the parameter skipped the check and the tool ran. Refusal only ever happened on a present-but-wrong token — an attacker did not need to forge anything, only to leave the parameter off. WHAT THAT MEANT IN PRACTICE, from the pre-fix test output: a `secure=True` tool invoked with NO token returned `{"response":"SECRET-BALANCE-9999"}` — the guarded payload, straight back over the wire. THE FIX. Entry into the validation block is now "a session manager exists and the function is registered"; an absent token is simply one way to FAIL validation rather than a way to SKIP it. Two things deliberately left alone: * THE `secure` PREDICATE IS THE EXISTING EXPRESSION, REUSED, NOT DUPLICATED (`func_entry.secure if hasattr(...) else func_entry.get("secure", True)`). A non-secure tool still runs with no token — there are two tests pinning exactly that, because getting this wrong would break every insecure tool in the fleet. * THE REFUSAL SHAPE IS UNCHANGED: the same FunctionResult dict at HTTP 200 as the invalid-token path. No status code was invented. mod_openai has NO handling for a SWAIG refusal status (grep for "invalid or expired" / "security token" across its .c files returns nothing), so the tool reports it cannot execute and the model relays that. A test asserts BYTE-EQUALITY of the absent and invalid refusals so no port can later conclude one of them may be an HTTP error. Verification — real HTTP through FastAPI TestClient against a real AgentBase with a real SessionManager minting genuine HMAC tokens. Nothing on the token path is stubbed. BEFORE (source stashed, tests in place): 4 failed, 5 passed test_absent_token_is_refused AssertionError: {"response":"SECRET-BALANCE-9999"} test_absent_token_does_not_leak_the_secure_payload test_empty_token_is_refused test_absent_and_invalid_refusals_are_the_same_shape The 5 that already passed are cases (i) valid-accepted, (ii) invalid-refused and both non-secure cases — which is what proves case (iii) was not bought by breaking the others. AFTER: 9 passed. FULL SUITE: 5730 passed / 3 skipped -> 5734 passed / 3 skipped, ZERO failures. The +4 is exactly the four assertions that were red. ruff format --check signalwire · ruff check signalwire · mypy (359 files) — all clean. I CORRECTED THE LANE'S BASELINE CLAIM. It reported "9 failed / 5774 passed" at 81d412e and attributed the failures to a pre-existing structlog-on-stdout defect in tests/test_examples.py. That is a measurement artifact: `git stash` does not remove an UNTRACKED file, so its own new test file stayed on disk and ran against the stashed-out source. Measured properly, the clean-tree baseline is 5730 passed / 3 skipped with no failures, and the after-state is 5734 with none. ONE FURTHER BEHAVIOURAL CHANGE, DELIBERATE: a PRESENT token with `call_id is None` previously skipped validation entirely; it now refuses for secure tools, because a token that cannot be validated is not a validated token. No existing test depended on the old behaviour. NOT CHANGED, FLAGGED INSTEAD: tests/unit/.../test_web_mixin.py:1718 test_valid_function_name_passes registers a dict with no `secure` key (so `.get("secure", True)` makes it secure) and posts with no token. Its behaviour genuinely changed — it now receives the refusal — but it still passes, because its assertion is `if hasattr(result, 'status_code'): assert result.status_code != 400` and a refusal dict has no status_code. That vacuity is pre-existing and the test's real subject is function-name validation, so rewriting it would be scope creep. SEPARATE DEFECT FOUND, NOT FIXED HERE — WORTH ITS OWN LANE: the SERVERLESS SWAIG path has NO token validation at all. core/mixins/serverless_mixin.py:224 _execute_swaig_function checks the function name and registry membership, then dispatches; no token is ever read. So `secure=True` is unenforced under lambda / cloud-function / azure. It is a different code path from _swaig_pre_dispatch and was outside this change's scope. CONSEQUENCES FOR THE COORDINATED PASS: this is REFERENCE SOURCE, so both oracles need regenerating and all nine ports re-drifting. porting-sdk 04e24f2's `token_absent` corpus golden is derived LIVE from the reference and will move with this change automatically — it must never be hand-edited. Note cpp already fails CLOSED (403) and is therefore now closer to the contract than the reference was; six ports (go, java, php, ruby, perl, dotnet) still never validate at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…valid, not merely ignored
OWNER-RULED 2026-07-29: "if the server doesn't read them, remove them."
THREE INDEPENDENT SOURCES AGREE THAT `body` ON A WEBHOOK IS WRONG:
* THE SPEC FORBIDS IT. porting-sdk/schema.json $defs/Webhook declares exactly ten properties —
error_keys, expressions, foreach, headers, input_args_as_params, method, output, params,
require_args, url — under `unevaluatedProperties: {"not": {}}`. `body` is not among them, so
emitting it is a SCHEMA VIOLATION, not a harmless extra.
* THE ENGINE NEVER READS IT. mod_openai/actions.c:735-739 and bedrock.c:4920-4926 each read
url, method, form_param, `params`, `headers` and nothing else. Stronger than that:
`grep -n '"body"'` across BOTH files returns ZERO matches, so `body` appears nowhere in
mod_openai at all.
* THE HELPER SILENTLY DISCARDED THE CALLER'S DATA. create_simple_api_tool accepted `body=` and
forwarded it to DataMap.body(), which wrote a key nothing consumes.
REPRODUCED BEFORE FIXING, not inferred:
create_simple_api_tool(..., method='POST', body={'query':'Q'})
-> webhook KEYS: ['body', 'method', 'output', 'url']
An invalid key on the wire carrying the caller's payload into a void.
AFTER: KEYS: ['method', 'output', 'url']; `body` is absent from the signature; passing `body=`
raises TypeError.
ALSO CORRECTED — A FALSE DOCSTRING THAT HAS ALREADY PROPAGATED. params() described itself as an
"alias for body". It is not: the two write DIFFERENT KEYS and only `params` is ever read. That
sentence has been copied verbatim into signalwire-cpp/include/signalwire/datamap/datamap.hpp:90,
which is plausibly how cpp's datasphere skill picked the wrong method (fixed earlier today in
cpp 07054db). The docstring now states the distinction and cites the schema and the engine readers.
The module docstring example at the top of data_map.py taught `.body(...)`; it now teaches
`.params(...)`. docs/api_reference.md drops `body` from the signature line and the parameter list.
SCOPE HELD DELIBERATELY: `DataMap.body()` — the public BUILDER METHOD — is NOT removed here. The
ruling was about "that call", i.e. the helper's parameter. Removing the builder is a larger,
separately-breaking change and belongs to its own decision. Measured cost if it goes: it is genuine
port surface recorded in python_signatures.json and implemented by ALL NINE PORTS (`Body` in
go/dotnet, `body` elsewhere) — roughly 24 test call sites, 7 example call sites, 18 doc references
and 6 internal factory forwards across 10 repos, plus an oracle regen and 9 port PRs. After this
commit, NOTHING in the reference's production code calls DataMap.body(): the discard site deleted
here was its only internal caller. Remaining reference references are 3 tests and 3 doc examples.
NOTE FOR THE BATCHED PORT PASS: each port's own create_simple_api_tool equivalent carries the SAME
body-forwarding bug, so the ports must be touched regardless of how the builder-method question is
ruled.
Verification:
BEFORE 5734 passed / 3 skipped / 0 failures (verified independently on the clean tree)
AFTER 5736 passed / 3 skipped / 0 failures — the +2 is exactly the two new tests.
ruff format --check signalwire -> exit 0, 215 files already formatted
ruff check signalwire -> exit 0, All checks passed
mypy --config-file pyproject.toml -> Success: no issues found in 359 source files
No oracle regenerated and no port touched — both are deliberately batched into the single
coordinated pass alongside 7c2f253 (secure=True requires a token).
A SECOND RULED ITEM TURNED OUT TO BE ALREADY DONE: task #140 (strip_control_chars becomes a real
1-param function with a structlog adapter) was landed by the owner on 2026-07-28 as b1ab620.
logging_config.py:33 is already `def strip_control_chars(event_dict)`, the private `_as_processor`
adapter supplies the protocol at both registration sites (:205, :233), `_drop_internal_keys` was
correctly left alone, and tests exist at tests/unit/core/test_logging_config.py:416-510. No change
was made for it here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…counting private symbols porting-sdk 481b435 corrected DOC-SURFACE's python decl regex, which matched a LEADING UNDERSCORE and so counted `_helper`, `__init__` and every dunder as public surface — 511 of 1539 symbols, a third of the denominator. Excluding them the way go/typescript/java/cpp already do, coverage is 70.6% (726/1028), not 64.5% (993/1539). This RAISES the bar. The gate had been RED since before today (64.5% vs a 64.7% floor) and report-only, so nobody saw it; the drop traced to cdd0b17 (ai_chat) and b1ab620 (strip_control_chars), and 9 of the 12 symbols that caused it were private. The floor now describes the corrected measurement — leaving 64.7 would have pinned a number the gate no longer produces. Gate exit 0 at the new floor. Full rationale and the per-port before/after in 481b435. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Every typed RELAY event wrapper's `from_payload` classmethod now documents which wire
event it parses and which fields it lifts out of `params`. These are what nine porting
teams read to know the RELAY event contract, and the non-obvious mappings are exactly what
was undocumented:
CallReceiveEvent `context` falls back to the payload's `protocol` field — older RELAY
servers name the same value `protocol`
RecordEvent url/duration/size read from the nested `record` object when present
and from the top of `params` otherwise, because RELAY reports them in
either position depending on event stage
CollectEvent `final` stays None when the payload omits it — absent is not False
DialEvent correlated by `tag`, not `control_id` like the other operations
ConferenceEvent conference-scoped, so the inherited `call_id` may be empty
MessageStateEvent carries `reason`, which is what explains a failed `message_state`
TranscribeEvent reads its artifact fields only from the top level — unlike RecordEvent
there is no nested object
DOCS ONLY — no code, signature or behaviour changed.
Verified: 490 relay tests pass; ruff format --check and ruff check clean; file parses.
DOC-SURFACE for this file goes from 24 undocumented to 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Adds real docstrings to the 12 undocumented public symbols in the REST
client's shared base, the layer every generated resource inherits and the
nine porting teams read as the contract.
HttpClient raw HTTP verbs (get/post/put/patch/delete): each documents that
`path` is an absolute API path appended to the client's scheme://host base,
what is serialised where (params -> query string, body -> JSON), the {}
return for 204/empty bodies, and the SignalWireRestError /
SignalWireRestTransportError split. Records the idempotency asymmetry read
from status_is_retryable: GET/PUT/DELETE retry on the full retry_on_status
set, while POST/PATCH retry only on a transport error or a 429/503 throttle.
Notes which verbs accept no query params (put/patch/delete).
ReadResource/CrudResource/CrudWithAddresses resource CRUD (list/get/create/
update/delete/list_addresses): documents path composition from the
resource's own base_path (vs the caller-built absolute path the HttpClient
verbs take), so the two `get`s and the two `delete`s are no longer
conflatable. Records that `list` returns ONE raw page and does not follow
pagination links (paginate() does); that create/update send kwargs as the
JSON BODY where list sends them as the QUERY STRING; that update dispatches
on _update_method (PATCH default, PUT under FabricResourcePUT); that
resource delete is typed TItem but SignalWire delete endpoints answer 204,
arriving as {}; and that list_addresses hits the nested sibling collection
<base_path>/<id>/addresses and is untyped Any, unlike list's TList.
_AbortSignal.is_set: documents that cancellation is cooperative and polled
only BETWEEN attempts, so a request already on the wire is not interrupted.
Measured with the AST gap checker (porting-sdk 62d0e64): the two files in
scope went 12 gaps -> 0. No code, signature or behaviour changed; docs only.
No defects found while reading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Closes every DOC-SURFACE gap in the `search` lane. Measured with the AST
gap tool (public = no leading underscore, documented = ast.get_docstring):
18 gaps before (search_service.py 11, __init__.py 6, document_processor.py 1),
0 after.
The bulk of this lane is the OPTIONAL-DEPENDENCY structure, which was
entirely undocumented and is easy to misread:
- search/__init__.py 117-146: preprocess_query, preprocess_document_content,
DocumentProcessor, IndexBuilder, SearchEngine and SearchService in the
`else` branch are FALLBACK STUBS, bound only when numpy / scikit-learn /
sentence-transformers / nltk are missing. Importing them always succeeds;
calling or constructing one always raises ImportError naming the missing
packages. Each stub now says so and points at the real symbol it shadows.
- search_service.py: SearchRequest / SearchResult / SearchResponse are
defined TWICE — pydantic BaseModel subclasses when pydantic imported, and
plain __init__-assignment classes when it did not. Both sets are now
documented per-field, and each fallback says it performs no validation and
is only reachable via search_direct (no FastAPI means no HTTP route).
- add_security_headers / validate_host: the two `@app.middleware("http")`
handlers — header stamping (https-conditional) and Host-header allowlisting
returning a bare 400.
- get_authenticated / search / health: the auth-scheme provider and the two
route handlers, including that /health is registered WITHOUT the security
dependency and masks the pgvector connection string to "***".
- document_processor.py flush(): the closure inside _chunk_markdown_ast that
emits an accumulated chunk with hierarchy/section/line-range metadata and
resets the accumulator, dropping whitespace-only accumulations.
Docs only — no code, signature or behaviour change.
Verified: pytest 5785 passed / 9 failed (the pre-existing test_examples
JSON failures) / 9 skipped; ruff format --check 0; ruff check 0;
mypy Success (359 source files).
Closes the entire `cli` DOC-SURFACE lane. Docstrings written from reading each body; no code, signature or behaviour changed. cli/core/agent_loader.py (2) mock_serve, mock_run — the monkeypatches installed over SWMLService.serve()/ run() (and AgentBase's) while a module's main() is called, so loading an agent file for inspection configures its service without starting a web server; the receiver is captured for the caller and all arguments are ignored. cli/output/swml_dump.py (1) suppressed_print — installed as builtins.print so loaded agent code cannot contaminate stdout during a SWML dump; calls naming an explicit non-stdout file are forwarded to the saved original, the rest are discarded. cli/simulation/mock_env.py (8) MockQueryParams.get/items/keys/values and MockHeaders.get/items/keys/values — two distinct stand-ins for FastAPI request objects in serverless simulation. Documented the behavioural split: MockQueryParams matches keys exactly and case-sensitively, while MockHeaders lowercases on both construction and lookup, so its items()/keys() yield lowercased names and the caller's original casing is not recoverable. All four view methods return live dict views. cli/dokku.py (10) Colors, print_step/success/warning/error/header, prompt, prompt_yes_no, generate_password, main. Noted that print_error writes to stdout and neither raises nor exits; that prompt_yes_no treats any unrecognized input (a typo) as NO rather than re-prompting; that generate_password draws from secrets.token_urlsafe (OS CSPRNG) over the base64url alphabet and TRUNCATES to `length`, retaining ~6*length bits rather than 8*length. main documents the five subcommands (init/deploy/logs/config/scale) and app-name inference. cli/init_project.py (5) Colors, print_step/success/warning/error — sw-agent-init's own copies. Each Colors docstring states which CLI it belongs to and that the dokku.py copy is a separate class (dokku's additionally has MAGENTA), so neither is mistaken for a shared module. Measured (porting-sdk AST gap counter, run from this worktree): before 114 real gaps / 30 files -> after 88 / 25 files (-26, all in scope). Verification: pytest 5785 passed, 9 skipped, 9 failed (the pre-existing tests/test_examples.py "Invalid JSON" failures, unrelated to this change); ruff format --check and ruff check exit 0; mypy Success (359 source files). Nothing in scope left undone. No defect found in the code read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Adds real docstrings to every remaining DOC-SURFACE gap in signalwire/livewire/__init__.py, the LiveKit-compatible API layer. Each explains the LiveKit-side concept AND what it maps to on SignalWire, which is what a migrating user and the porting teams need. Documented (repeated names sit on different classes): _NoopTracker.was_logged / .reset (:116, :120) ChatContext.append (:164) RunContext.userdata getter (:271) Agent.session getter/setter (:346, :350) AgentSession.userdata getter/setter (:505, :509) AgentSession.history getter (:513) handler closure in _register_function_tool (:626) Measured with the AST checker (porting-sdk 62d0e64): fleet gaps 114 -> 104; livewire/__init__.py 10 -> 0. Behaviour worth recording, found while reading: - AgentSession.history is initialized empty and never appended to anywhere in the SDK; the platform owns the transcript. Documented as such rather than implying it fills. - RunContext.userdata reads through to the session, but the handler built by _register_function_tool always constructs RunContext(session=None), so tool handlers get the empty-dict fallback. - ChatContext.append stores its `text` kwarg under the `content` key. - The tool handler calls fn synchronously, so an async tool function would be stringified as an un-awaited coroutine. Docs only -- no code, signature or behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
The shared mock_relay fixture probed the HTTP health endpoint and, on a 200, returned
immediately — treating that as proof the server was usable. But HTTP and WS are two
DIFFERENT ports (HTTP defaults to WS+1000, 9773 vs 8773). A foreign process owning the
HTTP port makes the probe green, so the fixture skips spawning its own server, and every
test then dies with ConnectionRefusedError on a WS port nothing is serving.
OBSERVED LIVE, not theorised: during the parallel docs burn a sibling lane's mock_relay
held the fixed ports and exited between one lane's probe and its WS connect. That lane saw
2 failures + 6 errors in tests/unit/relay/test_connect_mock.py, all ConnectionRefusedError
on 8773, from a diff that touched only livewire/__init__.py. It correctly refused to call
that a flake and traced it to conftest.py:349.
THE FIX: _probe_health now takes the ws_port and additionally opens a TCP connection to it.
A mock is only 'already up' when BOTH the endpoint the health check uses AND the socket the
tests actually connect over are alive.
PROVEN WITH A NEGATIVE CONTROL rather than asserted — an HTTP-only squatter answering 200
on 9773 with nothing on 8773:
probe(http only) -> True <- the old logic: 'reuse it', then every test refuses
probe(http + ws 8773) -> False <- the new logic: spawn our own
and tests/unit/relay/test_connect_mock.py -> 11 passed.
This is the fixed-mock-port hazard CLAUDE.md warns about. The deeper fix — bind an ephemeral
port instead of hardcoding 8773 — is a larger change to a shared fixture and is NOT done
here; this stops the silent misdetection, which is the part that turns a port collision into
a mystery test failure in an unrelated lane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
f601032 added `import socket` at module scope while _ws_port_accepting already imports it locally, matching the file's existing style (_probe_health imports requests the same way). Ruff flagged it F401 unused. Verified the file is back to its exact pre-change lint baseline: 24 errors both at 83a9833 and now — these are pre-existing findings in this conftest, none introduced by the probe fix. tests/unit/relay/test_connect_mock.py -> 11 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents every AST-measured DOC-SURFACE gap under signalwire/signalwire/skills/,
written from reading each body:
claude_skills/skill.py (5)
replace_command - shell-injection substitution; returns stdout, or a
bracketed [command timed out/error: ...] placeholder
replace_indexed - expands $ARGUMENTS[N]; out-of-range erases to ""
replace_shorthand - expands $N; same lookup, applied after the bracketed
pass so an expanded $ARGUMENTS[N] tail can't re-match
make_handler - per-skill closure factory (why a factory: else every
handler in the loop sees the last skill)
handler - section-vs-body selection then shell/variable/argument
substitution and prefix/postfix wrapping
info_gatherer/skill.py (5)
get_parameter_schema - the three params added on top of SkillBase's
get_instance_key - overrides the base tool_name keying to key on `prefix`,
which is what actually differentiates two instances
setup - what it validates; returning False = skill not loaded
get_global_data - initial namespaced questionnaire state
register_tools - the two prefixed tools and the toggle_functions on
completion
google_maps/skill.py (1) GoogleMapsClient - Places/Routes wrapper; both methods
log-and-return-None rather than raising
mcp_gateway/skill.py (1) handler - per-(service, tool) closure forwarding to
_call_mcp_tool
registry.py (1) add_skill_to_schema - attributes read, AttributeError
treated as empty schema, other exceptions swallowed so
one bad skill can't abort the scan
MEASURED (AST gap script, from worktree root):
before 114 gaps / 13 under skills/
after 101 gaps / 0 under skills/
Docs only - no code, signature or behaviour changed. Nothing left undone in
scope; no defects found in the code read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Closes every DOC-SURFACE gap under signalwire/signalwire/core/. All 17 are
nested/inner functions (route handlers, middleware, decorator inner layers,
closures) that the AST measure counts as public surface. Measured with the AST
gap script: 114 real gaps fleet-wide -> 97; core/ 17 -> 0.
Documented, with the wire/security behaviour read out of each body:
- auth_handler.get_fastapi_dependency.auth_dependency — bearer-then-basic,
first match wins, secrets.compare_digest; the api_key parameter is accepted
but NEVER consulted on this path; failure = HTTPException(401,
"Invalid authentication credentials") + WWW-Authenticate: Basic even when
bearer is the configured scheme; optional=True returns
authenticated=False instead of raising.
- auth_handler.flask_decorator.decorated — bearer -> API-key header
(X-API-Key or security_config.api_key_header) -> Basic; failure RETURNS a
401 Response with body "Authentication required" and
WWW-Authenticate: Basic realm="SignalWire Service" (returns, not raises).
- security/webhook_middleware.make_webhook_validation_dependency.dependency —
raw body captured before any parser and stashed on request.state.raw_body;
every rejection path (non-UTF-8 body, missing header, bad signature) raises
the same bare HTTPException(403) with no body detail; the injected response
arg is unused because a returned Response does not short-circuit a
dependencies=[] entry.
- swml_service.as_router.handle_swaig — the /swaig endpoint available on ANY
SWMLService; documents the handler's status codes (401/415/413/400).
- swml_service.serve.handle_all_routes and mixins/web_mixin's two
handle_all_routes — the with/without-trailing-slash recovery and the
serve()-vs-get_app() split (get_app's variant only classifies and returns
204; serve's variant is what actually dispatches /swaig, /post_prompt,
/check_for_input, /debug_events and the routing callbacks).
- swml_service.make_verb_method / swml_builder.make_verb_method — per-verb
closure, None-valued kwargs dropped before the wire, sleep excluded
(bare integer, not an object); service returns add_verb's bool (False on
schema-validation failure), builder returns self for chaining.
- agent/tools/decorator's inner_decorator and decorator — how a python
signature becomes a SWAIG tool: kwarg pop-list, name/description fallback
order, type-inference handoff and the typed-handler wrapper; the class
decorator only stamps _is_tool/_tool_name/_tool_params for deferred
registration, so inference does not run at class-definition time.
- agent/tools/type_inference.create_typed_handler_wrapper.wrapper — the
(args, raw_data) calling convention adapter; no validation or coercion.
- agent_base.enable_sip_routing.sip_routing_callback — always returns None on
every branch, so it never emits the 307 the routing contract allows; the
username match is observational logging only.
- config_loader.substitute_vars.replacer — ${VAR} / ${VAR|default} expansion;
missing var with no default yields empty string, never an error.
- mixins/web_mixin's two add_security_headers — nosniff / DENY /
strict-origin-when-cross-origin always; HSTS only when SSL is on.
- mixins/web_mixin.setup_graceful_shutdown.signal_handler — SIGTERM+SIGINT,
cleanup body is a no-op placeholder, always sys.exit(0), no request drain.
Notable behaviour found while reading (documented, NOT changed — docs-only
lane): the three "path not found" / "invalid route" catch-all responses in
swml_service.serve and web_mixin return FastAPI's default 200 status with an
error JSON body rather than a 404, so a caller must inspect the body to
detect a miss. Ports mirroring these handlers should be aware this is the
current reference behaviour.
Verification: pytest 5785 passed / 9 failed (only the pre-existing
test_examples "Invalid JSON" cases) / 9 skipped; ruff format --check exit 0;
ruff check exit 0; mypy Success (359 source files).
…ecurity middleware) Docs-only. No code, signature or behaviour changed. ai_chat/client.py (4): the three public response models ConversationInfo / ChatResponse / ChatLog now document what each field carries and where it comes from (notably that `id`/`conversation_id` are echoed from the request argument, not read from the response), and `close` says it only closes a client-owned session and that the client stays usable — the next request lazily rebuilds one. relay/call.py (6): is_done/stop/pause/resume/volume are on the Action handle hierarchy (Action -> StoppableAction -> PausableAction -> VolumeAction), NOT on Call; each now names the `calling.<prefix>.<cmd>` command it posts and the state it requires. Two facts taken from the engine (mod_infrastructure/relay_apis.c): `pause(behavior=)` is a closed enum "skip,silence" accepted only on record.pause (play.pause has no behavior field), and `volume` is gain in DECIBELS validated to [-40, +40] and required — not a 0-to-1 multiplier. `rank` is the lifecycle-order helper inside _wait_for_state; documented incl. the -1 unknown-state sentinel. pom/pom.py (2): build_section and recurse are nested closures, not rendering internals — build_section is the from_json/from_yaml validator that builds the Section tree (documented incl. every ValueError it raises), recurse is find_section's depth-first exact-title search. web/web_service.py (3) + mcp_gateway/gateway_service.py (3): the security middleware now states the exact headers set. web_service uses SecurityConfig.get_security_headers (nosniff / DENY / XSS / Referrer-Policy, HSTS only on https AND when enabled); mcp_gateway hardcodes its own set which adds a `default-src 'none'` CSP, omits Referrer-Policy, and has a non-configurable HSTS. validate_host rejects with 400 unless the host is in allowed_hosts — with two documented pass-throughs: a missing Host header is not checked at all, and "*" in allowed_hosts permits everything. `decorated` is _check_auth's wrapper (Bearer then Basic, both hmac.compare_digest, 401 + WWW-Authenticate on failure); `handle_error` is the catch-all that returns a fixed opaque 500 and never leaks the exception. Measured coverage (AST: public = no leading underscore, documented = ast.get_docstring): 114 -> 96 real gaps fleet-wide; all 18 in scope closed, zero left in these 6 files. Verified: pytest 5785 passed / 9 failed (only the pre-existing tests/test_examples.py "Invalid JSON" set) / 9 skipped; ruff format --check, ruff check, and mypy all exit 0. Brief corrections: pom's build_section/recurse were briefed as "rendering internals" — they parse/validate and search respectively, nothing renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…l is documented
BURN COMPLETE. 70.6% -> 100.0% (1102/1102), zero remaining gaps.
The move splits into two halves, and only the second half is documentation:
MEASUREMENT (70.6 -> 89.7, no docstrings written). Three independent defects in the
gate, all found by trying to burn against its output:
porting-sdk 481b435 counted _private + dunders as public surface (511 of 1539)
porting-sdk f8f58a2 could not see a docstring behind a WRAPPED SIGNATURE (161 false
negatives — every one already carrying full Args:/Returns:)
porting-sdk 62d0e64 matched def/class INSIDE STRING LITERALS — scaffolding templates
and docstring examples (24 phantoms). Superseded the other two by
measuring python with the AST instead of regex.
The first file opened to start writing (core/function_result.py, nominally 16 missing)
turned out to be fully documented already. That is what stopped the burn and sent the
work at the gate instead.
DOCUMENTATION (89.7 -> 100.0, 114 real symbols). One commit here + seven lanes run in
parallel git worktrees:
relay/event.py 24 83a9833 every from_payload event parser
cli 26 3e69e8d
search 18 508d75e
misc 18 d460ff4
core 17 aed6e78
skills 13 c28213e
rest/_base 12 efbf3cf
livewire 10 4f43f76
WHAT THE LANES FOUND WHILE READING — the reason to write these by hand rather than
generate them. Each was read out of the source, not inferred:
* REST retry is ASYMMETRIC: GET/PUT/DELETE retry on the full retry_on_status set, but
POST/PATCH retry ONLY on transport errors and 429/503, never 500/502/504. The code
calls it "part of the pinned contract"; it was documented on none of the verbs.
* Resource delete is typed TItem but the endpoints answer 204, so _request returns an
empty dict — the type signature actively misleads.
* relay volume is GAIN IN DECIBELS, engine-validated to [-40,+40], not a 0..1
multiplier (from mod_infrastructure/relay_apis.c). A port modelling it as 0..1 is
wrong.
* The pause behavior argument is a closed enum accepted ONLY on record.pause;
call_play_pause declares no such field, yet it is shared via PausableAction.
* Catch-all "not found" responses return HTTP 200, not 404 — a client cannot detect a
routing miss from the status line.
* get_app() and serve() build DIFFERENT catch-alls; the serverless entry point
dispatches nothing and answers 204.
* AuthHandler.get_fastapi_dependency accepts an api_key parameter and never reads it,
while the Flask decorator does honour it.
* sip_routing_callback returns None on every branch, so the 307 redirect it appears to
offer can never fire.
* ai_chat ConversationInfo.id and ChatResponse.conversation_id are echoed from the
request argument, never read off the wire.
* The two security-header implementations diverge: mcp_gateway adds a CSP and omits
Referrer-Policy relative to web_service.
* validate_host never checks a MISSING Host header.
These are documented, not fixed — several deserve an owner ruling before nine ports
reproduce them.
TWO BRIEFS OF MINE WERE CORRECTED BY THE LANES, both verified at source afterwards:
* I said search's duplicate class sets key on the four search dependencies. They key on
BaseModel is not None (fastapi+pydantic) — an independent axis. There is also a third
path I did not know about: _SEARCH_AVAILABLE true but a submodule import failing
partially populates __all__, so names are ABSENT rather than stubbed.
* I called pom's build_section/recurse "rendering internals". Neither renders — one is
the from_json/from_yaml validator/constructor, the other a depth-first title search.
Verification of the merged tree, run here rather than delegated:
doc_surface --port python -> 100.0% (1102/1102), gate exit 0
ast gap count -> 0 across 0 files
pytest tests/ -q -> 5785 passed, 9 skipped, 9 failed
(the 9 are the known test_examples "Invalid JSON"
local-checkout defect, task #66 — unchanged)
ruff format --check signalwire -> 215 files already formatted
ruff check signalwire -> All checks passed
mypy --config-file pyproject.toml -> Success, 359 source files
A 100% floor means the next undocumented public symbol reds the gate. That is the point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…was not
The two spellings of the SAME type disagreed. A SWAIG tool written in modern PEP 604 syntax
emitted a schema demanding a parameter its author had explicitly made nullable.
def h(city: Optional[str]) -> None: ... -> required=[] correct
def h(city: str | None) -> None: ... -> required=['city'] WRONG
ROOT CAUSE, core/agent/tools/type_inference.py:41. `_resolve_type` detected optionality with
if origin is typing.Union:
`Optional[str]` and `Union[str, None]` do carry `__origin__ is typing.Union`. But `str | None`
is a `types.UnionType`, a DIFFERENT object whose origin is not typing.Union — so the branch
never matched, the annotation fell through to the scalar path, `is_optional` came back False,
and the parameter was marked required. The fix accepts both:
if origin is typing.Union or isinstance(annotation, types.UnionType):
Verified across all three shapes, not just the failing one:
Optional[str] -> required=[] {"type": "string"}
str | None -> required=[] {"type": "string"} <- was ['city']
int | str -> required=['x'] {"type": "string"} <- genuine non-optional union,
still required, still the
documented string fallback
HOW IT WAS FOUND, because the mechanism is worth recording. tests/ has never been linted
(FMT/LINT target only signalwire/; EXAMPLES-* cover the example dirs; nothing covered tests/).
Bringing tests/ under the SDK ruleset ran `ruff --fix`, whose pyupgrade rules rewrote the
test file's `Optional[X]` annotations to `X | None`. Those annotations are not incidental
style in that file — they are the INPUT DATA to the function under test. The rewrite turned
`test_optional_type_is_not_required` red with `assert 'city' not in ['city']`, which is the
SDK bug stated exactly.
So the autofix did not break the test; it removed the only reason the bug was invisible. The
test suite had 5785 passing tests and none of them exercised PEP 604 optionality.
CONSEQUENCE FOR THE FLEET: this is reference behaviour, so every port's type-inference
equivalent needs the same question asked of it — does it recognise the language's modern
nullable spelling as optional, or only the legacy one? Filed rather than swept here, since
each port's answer is different (several have no equivalent syntax at all).
Verification:
pytest tests/ -q -> 5785 passed, 9 skipped, 9 failed
(the 9 are the known test_examples "Invalid JSON" local-checkout
defect, task #66 — the type-inference failure is gone)
ruff check signalwire -> All checks passed
mypy --config-file pyproject.toml -> Success, 359 source files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…mechanical pass
Owner-ruled 2026-07-29: cover the whole repo, at the strictness the SDK already gets.
BEFORE THIS, tests/ WAS NOT LINTED OR FORMATTED AT ALL. FMT/LINT target only `signalwire/`;
EXAMPLES-FMT/EXAMPLES-LINT cover the three example dirs; nothing covered tests/, scripts/ or
eng/. That is not theoretical: earlier today I added an unused `import socket` to
tests/unit/relay/conftest.py and no gate caught it — I found it by running ruff by hand.
MEASURED START, under the SDK's own select list
(E4,E7,E9,F,B,S,C4,PERF,SIM,PTH,RET,RUF,UP):
tests/ 1291 findings, 95 of 144 files would reformat
eng/ 9 findings
scripts/ 1 finding
CARVE-OUTS FOR tests/**, owner-ruled to match what examples/** already gets, plus the
pre-existing S101. Each is a test IDIOM, not a defect:
S101 a test's whole job is `assert`.
E402 tests/conftest.py MUST `sys.path.insert(0, project_root)` BEFORE importing
signalwire.*, so imports-not-at-top is structurally forced. Verified at
source (conftest.py:25-27 then :30+), not assumed. 302 of the 1291.
S104 binding 0.0.0.0 is how a server test says "listen on all interfaces".
S105/S106 literal "password"/"token" values ARE the fixture, not leaked secrets.
EVERYTHING ELSE STAYS ENFORCED — F401 unused imports, F841 unused variables, SIM117,
RUF015/RUF012/RUF059, UP035/UP045, E702, and every other bandit rule still red the gate. The
carve-outs took 1291 -> 785; they did not make tests/ green.
THIS COMMIT IS THE MECHANICAL HALF: `ruff check --fix` (safe fixes only, never
--unsafe-fixes) plus `ruff format`, run to convergence — autofix, format, autofix again,
format again, until both are stable. 308 findings fixed, 106 files reformatted, 147 files now
format-clean.
785 -> 474 remaining, all requiring human judgment. They are NOT suppressed and NOT
allow-listed; they are the next commit's work. The gate is deliberately NOT wired yet —
burn to zero before wire, per the standing rule.
A REAL SDK BUG FELL OUT OF THIS AND IS FIXED SEPARATELY IN 8066297. The pyupgrade rules
rewrote the type-inference tests' `Optional[X]` annotations to `X | None` — annotations that
are the INPUT DATA to the function under test, not incidental style — and
test_optional_type_is_not_required went red. That was not the autofix breaking a test; it was
the autofix removing the only reason a real bug was invisible. `str | None` was reported
REQUIRED while `Optional[str]` was not, because `_resolve_type` matched only
`typing.Union` and not `types.UnionType`. 5785 tests had never exercised PEP 604 optionality.
Verification, run at each step rather than once at the end:
pytest tests/ -q -> 5785 passed, 9 skipped, 9 failed
The 9 are the known tests/test_examples.py "Invalid JSON"
local-checkout defect (task #66), unchanged from the pre-work
baseline. Nothing new is red.
ruff check tests scripts eng -> 474 (from 1291), no fixable remainder
ruff format --check tests scripts eng -> 147 files already formatted
ruff check signalwire -> All checks passed (unaffected)
mypy --config-file pyproject.toml -> Success, 359 source files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
35 findings, all in tests/: * RET504 (12) — factory/fixture helpers that assigned then immediately returned. Returned directly; every one is a short helper where the intermediate name carried no meaning. * SIM105 (12) — try/except/pass -> contextlib.suppress(...). Each site is a deliberate ignore: async task-cancellation cleanup, fixture teardown, and control cases that assert on the request journal rather than the exception. * SIM115 (4) — tests/unit/search/test_search_engine.py setup_method used NamedTemporaryFile(delete=False) purely to mint a temp PATH, then closed the handle on the next line and never touched it again. Replaced with tempfile.mkstemp + os.close(fd), which drops the lingering handle and a dead self.tmp_file attribute. This file already documents the same reasoning at TestSearchEngineInit. * SIM102 (6) — collapsible nested `if` in tests/test_examples.py; all six are `if returncode != 0:` wrapping a single message-sniffing `if`, no else and no intervening statements. * SIM103 (1) — if/return True/return False folded to the condition. SIM117 (88) is not in this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
99 collapses closing the 88 findings ruff reported: collapsing an outer/inner
pair exposes the next level of a 3- and 4-deep chain, so it took three passes.
Every site is now the parenthesised multi-context form `with (\n a,\n b,\n):`,
which is what `ruff format` wants, so FMT stays a no-op.
Safety: `with A(), B():` evaluates A(), enters it, THEN evaluates B() — the
same ordering as nesting — so a collapse is behaviour-preserving unless the
inner header depends on a name the outer binds. Audited all 88 mechanically
(AST: intersect the outer's `as` targets with the names in the inner header):
zero dependencies. The context expressions are exclusively patch / patch.object
/ patch.multiple / patch.dict / pytest.raises, no exotic shapes.
8 comments sat BETWEEN an outer and inner `with` and had no home after the
collapse; each was re-attached to the with-item it annotated rather than
dropped. Verified by diffing the per-file multiset of comment lines against
HEAD: zero drift.
Verification (all from the worktree root):
ruff check tests scripts eng --select SIM,RET -> 0
ruff format --check tests scripts eng -> 147 unchanged
ruff check signalwire -> All checks passed
mypy --config-file pyproject.toml -> error set byte-identical
to the base commit (85)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
… (PUBLIC-JARGON) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…quest body
generate_comprehensive_post_data now emits exactly the field set of porting-sdk
swaig-specs/swaig-request.yaml (mod_openai actions.c execute_user_function): every
always-sent key (ai_session_id, app_name, argument{parsed,raw}, argument_desc,
call_id, channel_*, content_type, content_disposition, description, function,
version) plus the conditional keys a fully-configured call carries (caller_id_*,
project_id/space_id, global_data, meta_data_token+meta_data, call_log/raw_call_log).
Gone: the invented call / vars / params / prompt_vars / swml_env / request_headers /
http_method / webhook_url / user_agent / swaig_* flags / meta_data envelope. The CLI
passes the function's description and parameter schema (argument_desc).
tests/unit/cli/test_swaig_request_simulator.py validates the body (and the body after
the CLI mappings) against the vendored spec, closed at the root; negative controls:
invented keys and missing required keys are rejected (14 of 16 fail on the old code).
docs/cli_guide.md: the full-data section describes the engine shape.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…fast request times out Diagnostic only, for the one-off CI 3.13 timeout of TestPerRequestConfiguration::test_a_blocked_callback_doesnt_hold_up_other_calls seen in run 36384280432: on TimeoutError the test now pytest.fails with _dump_stacks() (every thread's stack and every asyncio task's awaits), so the next occurrence says where the fast request was waiting. The assertion is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
A class-form TypedDict cannot declare a key that is a Python keyword or
not an identifier. The generator used to drop those keys to a
"# non-identifier field" comment, so ConnectConfig["from"],
CondItem["else"], Expression["nomatch-output"] and 60 more were
untyped. porting-sdk/scripts/generate_python_rest_types.py now emits
each such declaration in the functional form,
X = TypedDict("X", {...}, total=False), with the annotations quoted and
the docstring assigned to X.__doc__.
"non-identifier field" comments: 63 before, 0 after.
The change only widens the types. Every key that was declared before
is still declared with the same annotation, totality and docstring, and
the only additions are the keys that had been comments. No entry point
signature changes.
tests/unit/core/test_swml_keyword_keys.py covers:
- the new keys type-check;
- a wrong "from" type is rejected (a negative control held by
warn_unused_ignores);
- the runtime shape of the functional form;
- old-style calls still type-check: plain dicts to set_params and
set_param, arbitrary ai() kwargs, and connect's existing keys.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
`SWMLBuilder(service).label("start")` produced no verb and no error: verb
methods now take a positional body (`verb(config)`), `_verb_body` passes a
non-mapping body through, and `add_verb` rejects it with only an
`invalid_config_type` log line. On main the same call raised `TypeError`
(verb methods took keyword arguments only).
When `add_verb` does not add a positional non-mapping body, the generated
verb methods (SWMLBuilder and both SWMLService paths) now raise `TypeError`.
Bodies add_verb accepts are unchanged: object/keyword bodies and sleep's
integer form. Non-breaking vs main: the call that raised there raises again.
Tests (real SWMLService, no mocks): builder + service refuse `label("start")`
(RED before this change: DID NOT RAISE); keyword, mapping and sleep(1000)
bodies render as before; an invalid keyword body still raises
SchemaValidationError, as on main.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
Owner ruling 2026-09-28: fsvars is never available in production and must not appear in public specs. mod_infrastructure 694f6094 tags every fsvars declaration @api-hidden (the engine ignores it unless FREESWITCH_DANGER_ALLOW_FS_VARS is set, which production never sets). Reverts the fsvars half of 7ec985c; username/password stay. Non-breaking vs main f870cc1, whose answer() never had fsvars. Depends on the vendor chain: the bundled schema.json still publishes answer.fsvars, so test_hand_written_verb_accepts_every_public_spec_param [answer] fails until schema.json is re-bundled without it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
fsvars is hidden from every public spec (owner ruling 2026-09-28: a staging-only FreeSWITCH-variable hack, gated off in production by danger_do_allow_customer_to_set_fs_vars). Re-bundled with port_schema_bundle.py sync; regenerated types (swml_verbs_generated drops fsvars from AnswerConfig/ConnectConfig/ConnectDevice). The union-key pin for connect goes 32 -> 31: the only key the spec removed is fsvars. Non-breaking vs main (f870cc1): main's bundled schema.json has no fsvars, so `answer/connect(fsvars=...)` raised SchemaValidationError there and still does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…+ 32 ops in fabric/whatsapp/relay-rest
Regenerated from porting-sdk rest-apis (hand-added, verified against prime-rails@29d6dce4b550).
All additive: new resources, new methods, one new optional constructor argument; no existing
signature changes.
* RestClient(personal_access_token=... / SIGNALWIRE_PERSONAL_ACCESS_TOKEN): client.space is
wired to a PAT HttpClient (HTTP Basic, empty username); a client may hold either
credential or both, and a resource whose credential is missing raises ValueError.
* HttpClient: per-call headers on get/post; get_text (non-JSON success) and
get_redirect_location (a success that IS a redirect) for billing_statement.csv / .pdf.
* client.space.{settings,geographic_permissions,billing_profile,billing_statements,usage,
payment_history,members,balance,low_balance_setting,payment_methods}.
* client.fabric.{alias,sip,phone_number}_addresses, fabric.addresses.delete,
fabric.resources.assign_sip_endpoint / assign_whatsapp_number.
* client.whatsapp.{numbers,businesses,templates}.
* addresses.update, registry.brands.update, phone_numbers E911 + CNAM methods.
* Tests: generated wire tests (success + error per route) over the shared mock, now with
a per-test PAT; test_space_mock.py for the PAT credential, text/redirect reads and the
Idempotency-Key header.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…dk base (additive / widening only) porting-sdk corrected fabric and relay-rest base facts from prime-rails@29d6dce4b5. The python effect is additions and widenings: new optional params and response fields, and required params the server does not require made optional. No method, param or field is removed or renamed. Tests: test_code_verified_rest_fields.py sends each newly declared field over the shared mock and asserts no STRICT-MOCKS wire violation (9 of 10 fail against the previous base). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…s only) Composed specs now carry the DevEx prose harvest (api-reference-specs 95829db, all draft). The generator reads schema descriptions only, so 4 generated modules gain TypedDict docstrings; no type, field or signature changes (python_signatures.json and python_surface.json regenerate byte-identical). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…ditive / widening only) porting-sdk e79dec7 corrected datasphere, fabric, messages, projects, relay-rest, space and video base facts from prime-rails@61268748c7. The python effect is additions, widenings and required -> optional; every changed param is keyword-only. No method, param or field is removed or renamed: * messages.create(): message_type, template_id, header/body/button_template_parameters; body also accepts dict | list. * fabric.tokens.create_subscriber_token(): scope, fingerprint. * fabric.conference_rooms.create(enable_room_previews=), video.streams.update(url=), space.settings.update(name=) are optional. * video date fields (join_from / join_until / remove_at) accept str | float. * response types: AvailablePhoneNumber +e164/national/international_number_formatted/ country_code, AssignedNumber +status_callback_url, CXMLScriptResponse +display_name, ActiveSession +sync_audio_video, Recording union +RelayConferenceRecording; ProjectUpdate is its own TypedDict (same keys as before). Tests: test_code_verified_rest_fields_round2.py sends the new/loosened fields over the shared mock and asserts no STRICT-MOCKS wire violation (the 3 new-field tests fail against the previous base). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…ed from the REST prose)
Owner ruling 2026-09-29: the Compatibility API is not in the SDKs and the rest of the specs must
not reference it. The only change is the RelayVoiceLog docstring ("Voice log for cXML and
Relay call types" instead of "... for Compatibility and Relay ..."). python_signatures.json and
python_surface.json regenerate identical (ORACLE-FRESH: both FRESH against this tree).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…-bundle (psdk b81e637 / ARS 0fd0e3b) Owner ruling 2026-09-29: "we will not be including compat api in sdks at all.. we will make a completely seperate public spec that is kept out of the main index, so its not private, but the rest of the specs shouldnt reference. customers will not be able to get to any compat api without approval". - docs/security.md: drop "migrating from the old `@signalwire/compatibility-api` shape"; "cXML and other compatibility endpoints" -> "cXML webhooks"; the X-Twilio-Signature alias described as applying on cXML requests. - core/security/webhook_validator.py validate_request / webhook_middleware.py module docstrings: same rewording (docstrings only). - mcp/swml-schema-search/README.md sample output: join_conference line. - schema.json re-bundled (port_schema_bundle.py sync); swml_verbs_generated.py JoinConferenceConfig docstring regenerated. REST regen: no change. Docstring/doc-only: no signature or surface change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
… not breaking vs 3.x main) Regenerated from porting-sdk 3d1defd (REST base: held set + audit BASE-BUG fixes, embedded SWML generated from schema.json with dial/eval/if withheld; schema.json from api-reference-specs 6610c1f: enter_queue.wait_time default 180). A/B against f870cc1 (1071 baseline-working calls): 1049 identical, 6 wire changes (the plural call_flows/conference_rooms URLs the server routes; calling.record audio sent as params.record.audio), and 16 calls that omit a field the server rejects without (422 today: cxml_scripts name, sip_endpoints password, queues name, campaign order phone_numbers) -- every one of them still works when the field is passed via extras. - rest/_base.py: _required_via_extras -- a required argument supplied through extras={...} satisfies the requirement (generated methods are decorated where it applies). - calling.record(audio=) -> params.record.audio; new optional record=. - core.logging_config.strip_control_chars(*args): the 1-arg and the structlog 3-arg forms. - Deprecated aliases for the 1:1 renamed generated types (24 swml_verbs_generated, 3 swml_webhooks_types_generated, cli.types.PostData); ConnectDevice*/Contexts stay callable; CHANGELOG lists the names with no single replacement and SwmlRequestCall. - New: AiAgents.list_voices() / list_conversation_logs(). - Tests: plural fabric sub-paths, record wire shape, required-via-extras unit tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…po variable
The coordinated-pass pin was the repo variable PORTING_SDK_REF, which every
branch's CI reads -- main and everyone else's PRs were checked out against our
in-progress porting-sdk branch. The variable has been deleted; the pin now lives
in a file committed only on the coordinated branch:
- .porting-sdk-ref (this branch: wave6/ctor-dunder-fold). No file -> main, so
main and every other branch are unaffected. DELETE it in the PR that merges
the coordinated set (porting-sdk/COORDINATED_PASS.md).
- .github/coordinated-ref.sh: byte-identical copy of porting-sdk's canonical
resolver. A `coord` step after the repo's own checkout validates the pin
(branch-name characters only) and emits ref / ref_<name> outputs.
- every porting-sdk / signalwire-python checkout takes
${{ steps.coord.outputs.ref }} / ref_python instead of the variable; publish
and release workflows stay hard-pinned to main.
- run-ci.sh: COORDINATED-REFS description + comments name the pin file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…ask #51) Regenerated from porting-sdk's composed REST specs: - fabric AI agent: prompt/post_prompt/languages/SWAIG/pronounce/hints/params typed from the SWML `ai` verb plus prime-rails' transforms (AIAgentPrompt, AIAgentPostPrompt, AIAgentLanguage, AIAgentSWAIG, AIAgentSWAIGFunction, AIAgentSWAIGInclude, AIAgentPronounce). The 157 fabric type names no fabric operation uses keep importing as deprecated aliases (155; CHANGELOG lists the two with no replacement). - calling.* command params from the RELAY calling methods (relay_apis.c): new optional kwargs where the engine accepts more; Relay* sub-schema types. control_id is generated when omitted on play/record/collect/detect/tap/stream/transcribe (x-sdk-autofill) -- the engine rejects those commands without one. A/B vs f870cc1 (every generated method, the ways a 3.x caller can call it): 1071 working cases, 1009 same wire, 16 break (unchanged: the server-required fields ruled before), 46 wire diffs = 40 with a generated control_id added + the 6 earlier intended changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
The branch-local pin (.porting-sdk-ref, optional .signalwire-<name>-ref) moves from the repo root into .github/ so signalwire-python's ROOT-HYGIENE stays strict with no allowlist entry (owner, 2026-09-29). Re-copies the canonical .github/coordinated-ref.sh, which now reads .github/porting-sdk-ref and fails on a pin at the retired root spelling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…dden Regenerated from porting-sdk wave6/ctor-dunder-fold (ARS 381b8b1 round trip): - Calling gains ai_sidecar, ai_sidecar_ask, ai_sidecar_poke, ai_sidecar_stop and ai_sidecar_status -- the calling.ai_sidecar / .ask / .poke / .stop / .status commands POST /api/calling/calls accepts (mod_infrastructure relay_apis.c JSON_CHECK call_ai_sidecar*, whose key lists include async). Params composed from the engine's allowlists; ai_sidecar takes a `params` kwarg, so the generator names its local `command_params` there. - Project (projects_types_generated) drops region_preference: feature-flag gated, owner 2026-09-30: hidden until the region_preference feature is released (flag GA). - Mock-backed wire tests for the five commands in tests/unit/rest/test_calling_mock.py. A/B (every generated REST method of f870cc1, called every way it can be): the 1122 baseline-working cases produce the same wire on this tree as on b32c3dd (autofilled control_id uuids normalised); method set vs b32c3dd: +5, -0, 0 signatures changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
Brings PR #78 current with main 6bdf2eb (release 3.5.1). Conflicts, resolved by reading both sides: - signalwire/core/data_map.py, docs/api_reference.md, tests/unit/core/test_data_map.py: this branch removed DataMap.body() and create_simple_api_tool(body=) (b6f5b30, dafa0f1) because body() wrote a `body` key nothing reads. main (555dcbb, released in 3.5.1) instead made both send `params`, the key the platform reads. Taken main's side: body() and body= now work in a released version, so removing them would break working 3.5.1 code, and the removal's premise (the payload is discarded) no longer holds. Dropped the tests asserting their absence (TestBodyBuilderRemoved, test_create_simple_api_tool_rejects_body); kept test_create_simple_api_tool_emits_no_body_key. Needs the owner's confirmation. - CHANGELOG.md: [Unreleased] (this branch) above [3.5.1] (main). - pyproject.toml: both added the same mypy override for deprecated.*; main's text. - ai_chat/handoff.py, cli/simulation/mock_env.py: docstrings; main's, which describe its locking and the new `omit` parameter. - skills/native_vector_search/skill.py: main's TYPE_CHECKING import of _PublicSession; AgentBase dropped, as this branch had (unused). - tests (test_datamap_templates, test_bedrock_agent, test_function_result, test_mcp_gateway_skill, test_native_vector_search_skill): main's behavior with this branch's formatting and imports; test_bedrock_agent keeps this branch's "a voice Bedrock does not offer". Follow-ups for main's code under this branch's gates: - ruff format over the FMT/REPO-FMT scopes (12 files from main). - ruff check: import pytest in test_execution_mode.py (the merge kept this branch's removal of the import and main's new parametrize), a ClassVar in test_unicode_digits.py, one combined `with` in test_web_service.py, an unused noqa in test_documented_limits.py, and an explicit timeout= in the conftest session stub. - test_skip_prompt_guard.py stubs _PublicSession.get: main sends mcp_gateway's health check through the session, not requests.get. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
main's DataMap simulator (3.5.1) added _Run.log() and _Run.expand() without docstrings; this branch's DOC-SURFACE floor is 100%, and the merge left it at 1180/1182. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…nal, relay-rest fixes Regenerated from porting-sdk wave6/ctor-dunder-fold, whose REST base was checked against prime-rails' code-derived documents (#10337): - calling.dial: `codecs` is accepted with a URL as well as inline SWML, and `to` may be omitted when `to_script` is given (the Create contract needs one of the two). - messages.update: `body` is optional; the server defaults it to "" and an empty body is what redacts. - relay-rest: recordings.get returns the Recording union (conference recordings included); SIP endpoint call_handler accepts laml_webhook; SipEndpoint encryption in responses is required|optional; domain application call_flow_version drops current_deployed, which the server rejects for the call_flow handler and ignores otherwise. Every change widens or corrects types; an A/B of every generated method against f870cc1 shows no new break and no wire change. Tests: a dial with to_script and no `to`, and recordings.get sends Accept: application/json (prime-rails' recordings#show redirects to the audio file unless JSON is asked for). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
…n removed; soft AI entry-point typing Regenerated from porting-sdk wave6/ctor-dunder-fold (owner rulings 2026-09-30): - `client.recordings.download(id)` and `client.video.room_recordings.download(id, media_ttl=...)` return the URL the server redirects to (a presigned MP3 URL; the recording's signed MP4 URI), without following it. Mock-backed tests pin the path, the 302 and media_ttl. Every generated redirect method now documents that contract. - `verified_callers.create` gains optional `country_code` and `number_type`, which the server stores when sent. - `fabric.tokens.create_invite_token` and its types are removed: the route takes only a subscriber's Bearer token, so it never worked with the project credentials this client sends. Use `create_guest_token`. - AI entry points (option B, the soft form of the held 9d60694): `SWMLBuilder.ai(**kwargs)` gains an overload typed by the generated `_AiConfigKwargs` plus the old `**kwargs: Any` one; `set_param` is typed by the generated `_AiParamsSetters` overloads, which end in a `(key: str, value: Any)` fallback; `set_params` takes `AiParams | dict[str, Any]`. Every call that type-checked before still does (test_swml_keyword_keys unchanged, no new ignores); runtime and rendered SWML are unchanged (test_ai_entry_point_typing). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
Regenerated from porting-sdk wave6/ctor-dunder-fold (local e783401+): POST /api/relay/rest/verified_caller_ids `number_type` and `country_code` are overlay-hidden (owner 2026-09-30, option A; cloud-product#21368 -- client-settable trust inputs until the server derives them). They leave `verified_callers.create`, `CreateVerifiedCallerIDRequest`, and the unreleased CHANGELOG entry that announced them. Not breaking: both were added on this branch after f870cc1; the A/B over f870cc1's 1071 working calls matches 51896c2 exactly (the same 20 pre-existing breaks, 0 wire differences). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…ai a9a81f1) swaig_actions_generated.py: docstring/source-line moves only (no surface change). schema.json re-bundled from porting-sdk's ARS e892e84 copy (prose line moves). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…channel
porting-sdk 7af4651 (owner ruling 2026-09-30, R-CHANNELS option C): the POST
/api/fabric/resources/{id}/whatsapp_numbers response channels is oneOf {audio} | {messaging}.
The generator emits that union as dict[str, Any] (was dict[str, str]). Non-breaking:
WhatsappNumberAddressResponse and assign_whatsapp_number do not exist at f870cc1, and the
change only widens a response field's static type.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…ecc5
schema.json (and its .sha256 record) re-copied from porting-sdk: native truncated-valueint
floors (`> 0` on a truncated value -> minimum 1, `>= 0` -> exclusiveMinimum -1) and one
shared x-boolean-strings fact replacing x-boolean-string-accepts. Non-breaking: the tightened
bounds are what the engine already accepted (a fractional value below the floor was rejected
server-side by the truncating validator), and no python code reads the renamed extension.
fabric PhoneRouteResponse.channels regenerates as dict[str, Any] (oneOf {audio} |
{messaging}, prime-rails phone_routes handler); a static widening from the f870cc1 shape.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…arams Regenerated from porting-sdk 4192fc0 (relay-protocol re-extracted from switchblade feat/relay-contract-extractor 7b7dfdf). Additive optional keys on total=False TypedDicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
sentence-transformers (requirements-dev.txt) pulls torch, and PyPI's Linux torch brings ~3 GB of nvidia CUDA wheels the tests never use: python Test 36823015697 spent 06:06->06:59 in 'Install signalwire-python (editable + dev deps)' on 3.10/3.11 (gates themselves passed). Pre-installing torch from the PyTorch CPU index satisfies the requirement so pip keeps the CPU build. Applied to test, nightly, publish-dev, publish-release, and multi-os (Linux only; macOS/Windows PyPI wheels are CPU already). test.yml also caches pip keyed on pyproject.toml + requirements-dev.txt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…examples fix) porting-sdk schema.json moved 5b91fcf -> fce555f when the stringified @example values were fixed at source (api-reference-specs tsp_reader parse_value). Data only: the REST type generator does not read examples; GEN-FRESH stays fresh (2203 types). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JK98iDuVZQ6gQBo3Zj3RAb
…st[str]] Every return is (bool, list[str]) and the docstring says so; the annotation was the loose tuple[Any, ...]. Since porting-sdk b457b0d records a variadic tuple as list<any>, that loose annotation made every port that types the real (bool, list[str]) return drift. The three sibling validators (SWMLVerbHandler.validate_config, SchemaUtils.validate_document / validate_verb) already declare tuple[bool, list[str]]. Typing-only; no runtime change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JK98iDuVZQ6gQBo3Zj3RAb
This branch has not been deployed
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.
The reference SDK's half of the wave-6 coordinated pass. 49 commits on top of
main.This supersedes #77 — that PR's head (
fix/win-test-portability-tail) is a directancestor of this branch, so all seven of its commits (Windows portability, the multi-OS
interpreter fix, and the
builddeclaration PACKAGE-SMOKE needed) are already includedhere. #77 can be closed in favour of this, or left open and closed on merge; it has
nothing this branch lacks.
Why the oracle had to move
Two of these commits were the root cause of a fleet-wide CI red today. Every port had been
brought to the enforced SWAIG token contract, so each correctly REFUSES an untokened
serverless call — but CI built its oracle from a reference that still executed it, and
seven ports failed BEHAVIORAL-HTTP for being right:
e4f66b6— enforcesecure=Trueon all four serverless transports. The negative-halfcorpus fixture
http_serverless_lambda_swaigdeliberately carries no__token; thereference now refuses it, matching the ports.
c0183cd— keep SDK logs off stdout. structlog debug lines were interleaving with theJSON that Layer-D dumps parse.
Both sat unpushed on a local branch, which is exactly the staleness the coordinated-pass
mechanism exists to prevent. Confirmed fixed: ruby's re-run went from
CI FAIL (gates: COORDINATED-PASS BEHAVIORAL)to a clean pass.Also in here
71eed0c— removeDataMap.body()(breaking): the builder wrote a key nothing reads.The fleet-wide ripple is done; all ten ports have dropped it.
a23c85b+ the lint burn — REPO-LINT/REPO-FMT wired overtests/,scripts/andeng/,which were gated by nothing. Burned 1291 -> 0 before the gate landed, so it never lands red.
4371610— validatepost_promptshape; the validator was passing configs that abort the call.ee1911c— mypy 85 errors -> 11.2850adf— DOC-SURFACE floor pinned at 100%.Verification
Local
run-ci.shis green on this branch. The gates that matter for the coordinated set —SIGNATURES, DRIFT, SURFACE, EMISSION — all pass, and the two oracle artifacts regenerate
byte-identically.
Coordinated-With: porting-sdk@wave6/ctor-dunder-fold