Skip to content

Commit 5b84398

Browse files
committed
Clear the gates this branch was failing on its own
Four gates were red on wave6 independently of the main merge. TYPECHECK — 5 findings in signalwire/search/, present on this branch before the merge (verified against pristine origin/wave6). Two shapes, both real: * `if not SentenceTransformer:` / `if not Presentation:` — the optional-dep shim leaves the name as the class or None, and a class object is always truthy, so the guard never fired the way it reads. Now `is None` / `is not None`. * `model.model_name = model_name` — torch's Module.__setattr__ is typed Tensor|Module, so stashing the name for cache identification is a third-party stub gap. Narrow `# type: ignore[assignment]` with the reason. mypy is now clean over all 365 files. REPO-LINT — the wave's new gate lints tests/ too: an unused `Awaitable, Callable` import and a PERF401 append-loop, both in test_gateway.py. The loop is now an async comprehension. DOC-SURFACE — the floor is 100% here and coverage was 99.5%. Documented the remaining 8: AgentServer's add_security_headers / health_check / readiness_check, and ChatGateway's close / check_key / preflight / proxy / stream. Real docstrings — check_key notes the compare_digest timing property, stream notes that awaiting the body would swallow the keepalive padding the service sends to survive intermediaries. Now 100.0% (1108/1108). DOC-AUDIT — `get_stats` is a real SearchEngine method in signalwire/search/, which is excluded from the oracle by design; added to DOC_AUDIT_IGNORE.md under the Search-subsystem section that already names local_search_agent.py. Verified with the paired porting-sdk wave6 branch: the full run-ci is green except ROUTE-COLLISION, which fails only locally — python_route_registry.py resolves the SDK through _sibling_root and lands on a relative path when the registry runs with cwd=--repo. Handed an explicitly-built registry it reports route-split=0, crud-dup=0, orphan-dto=0. CI's layout resolves it and has never reported that gate.
1 parent 3f62f75 commit 5b84398

8 files changed

Lines changed: 61 additions & 12 deletions

File tree

DOC_AUDIT_IGNORE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ similarity_search: langchain/pinecone Pinecone.similarity_search — search comp
9898
build_index: real IndexBuilder.build_index (signalwire/search/index_builder.py) — Python-only search skill, absent from the cross-port surface
9999
build_index_from_sources: real IndexBuilder.build_index_from_sources (signalwire/search/index_builder.py) — Python-only search skill, absent from the cross-port surface
100100
migrate_sqlite_to_pgvector: real migration helper (signalwire/search/migration.py) — Python-only search skill, absent from the cross-port surface
101+
get_stats: real SearchEngine.get_stats (signalwire/search/search_engine.py) — Python-only search skill, absent from the cross-port surface
101102
argsort: numpy.argsort — DIY search example in docs/search_overview.md
102103
md: filename-extension regex false positive (matches `file.md (…`) in docs/search_overview.md processing listing
103104
do_search: user-defined method inside a caching example in docs/search_deployment.md

signalwire/signalwire/agent_server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ def __init__(
8080
async def add_security_headers(
8181
request: Request, call_next: Callable[[Request], Awaitable[Response]]
8282
) -> Response:
83+
"""Attach the standard hardening headers to every response.
84+
85+
Applied here rather than per-agent so a newly registered agent
86+
cannot ship without them.
87+
"""
8388
response = await call_next(request)
8489
response.headers["X-Content-Type-Options"] = "nosniff"
8590
response.headers["X-Frame-Options"] = "DENY"
@@ -647,6 +652,11 @@ def _register_health_endpoints(self) -> None:
647652

648653
@self.app.get("/health")
649654
def health_check() -> dict[str, Any]:
655+
"""Liveness probe: the process is up, plus what it is serving.
656+
657+
Returns the agent count and their routes, so a failed deploy that
658+
registered nothing is visible from the probe itself.
659+
"""
650660
return {
651661
"status": "ok",
652662
"agents": len(self.agents),
@@ -655,6 +665,7 @@ def health_check() -> dict[str, Any]:
655665

656666
@self.app.get("/ready")
657667
def readiness_check() -> dict[str, Any]:
668+
"""Readiness probe: the server is prepared to take traffic."""
658669
return {"status": "ready", "agents": len(self.agents)}
659670

660671
def _run_server(self, host: str | None = None, port: int | None = None) -> None:

signalwire/signalwire/ai_chat/gateway.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ def effective_timeout(self) -> int:
223223
return self.conversation_timeout or SERVICE_DEFAULT_CONVERSATION_TIMEOUT
224224

225225
async def close(self) -> None:
226+
"""Release the upstream HTTP session, if this gateway owns it.
227+
228+
A client passed in via `client=` belongs to the caller and is left
229+
open; only a client the gateway built for itself is closed here.
230+
"""
226231
if self._owns_client:
227232
await self._client.close()
228233

@@ -288,6 +293,17 @@ def check_origin(self, origin: str | None) -> None:
288293
raise GatewayRejection(403, "origin not allowed")
289294

290295
def check_key(self, presented: str | None) -> None:
296+
"""Verify the publishable key the browser sent.
297+
298+
Compared with `hmac.compare_digest` rather than `==` so the check does
299+
not leak the key a character at a time through timing.
300+
301+
Args:
302+
presented: Key from the request, or None when the header is absent.
303+
304+
Raises:
305+
GatewayRejection: 401 if the key is missing or does not match.
306+
"""
291307
if not presented or not hmac.compare_digest(presented, self.key):
292308
raise GatewayRejection(401, "bad key")
293309

@@ -454,6 +470,12 @@ def _cors(origin: str | None) -> dict[str, str]:
454470

455471
@router.options("/")
456472
async def preflight(request: Request) -> Response:
473+
"""Answer the CORS preflight for a listed origin.
474+
475+
Returns 204 with the allow headers when the origin passes, and a
476+
bare 204 with no CORS headers when it does not — a browser then
477+
refuses the real request without this ever seeing it.
478+
"""
457479
origin = request.headers.get("origin")
458480
headers = _cors(origin)
459481
if headers:
@@ -464,6 +486,13 @@ async def preflight(request: Request) -> Response:
464486

465487
@router.post("/")
466488
async def proxy(request: Request) -> Response:
489+
"""The one route a browser calls: check, rewrite, forward, stream.
490+
491+
Verifies origin and key, replaces whatever the body claimed with
492+
this gateway's own config_url and conversation id, forwards to the
493+
chat service, and streams the reply back untouched. A newly minted
494+
handle rides back on the `X-Chat-Handle` response header.
495+
"""
467496
origin = request.headers.get("origin")
468497
auth = request.headers.get("authorization", "")
469498
key = auth[7:] if auth.lower().startswith("bearer ") else None
@@ -522,6 +551,13 @@ async def proxy(request: Request) -> Response:
522551
headers["X-Chat-Handle"] = minted
523552

524553
async def stream() -> Any:
554+
"""Relay upstream bytes as they arrive, without decoding them.
555+
556+
The service pads a slow turn with whitespace so intermediaries
557+
do not sever the connection. Awaiting the whole body here would
558+
swallow that padding and recreate, inside the caller's own
559+
stack, the very timeout the padding exists to survive.
560+
"""
525561
async with self._client.raw_post(method, params) as resp:
526562
async for chunk in resp.content.iter_any():
527563
yield chunk

signalwire/signalwire/search/document_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ def _extract_excel(self, file_path: str) -> str:
364364

365365
def _extract_powerpoint(self, file_path: str) -> Any:
366366
"""Extract text from PowerPoint files"""
367-
if not Presentation:
367+
if Presentation is None:
368368
return json.dumps(
369369
{"error": "python-pptx not available for PowerPoint processing"}
370370
)

signalwire/signalwire/search/index_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def _extract_metadata_from_json_content(
165165
def _load_model(self) -> None:
166166
"""Load embedding model (lazy loading)"""
167167
if self.model is None:
168-
if not SentenceTransformer:
168+
if SentenceTransformer is None:
169169
raise ImportError(
170170
"sentence-transformers is required for embedding generation. Install with: pip install sentence-transformers"
171171
)

signalwire/signalwire/search/query_processor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,10 @@ def _get_cached_model(model_name: str | None = None) -> Any:
291291

292292
logger.info(f"Loading sentence transformer model: {model_name}")
293293
model = SentenceTransformer(model_name)
294-
# Store the model name for identification
295-
model.model_name = model_name
294+
# Store the model name for identification. torch's Module.__setattr__
295+
# is typed for Tensor|Module, so stashing a plain str on the instance
296+
# is a third-party-stub gap, not a real type error.
297+
model.model_name = model_name # type: ignore[assignment]
296298
# Evict oldest entry if cache is full
297299
if len(_model_cache) >= _MAX_MODEL_CACHE_SIZE:
298300
oldest_key = next(iter(_model_cache))

signalwire/signalwire/search/search_service.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,9 +528,10 @@ def _load_resources(self) -> None:
528528
)
529529
try:
530530
model = SentenceTransformer(model_name)
531-
model.model_name = (
532-
model_name # Store for cache comparison
533-
)
531+
# Store for cache comparison; torch's
532+
# Module.__setattr__ is typed Tensor|Module, so a
533+
# plain str is a stub gap, not a real error.
534+
model.model_name = model_name # type: ignore[assignment]
534535
self.models[model_name] = model
535536
except Exception as e:
536537
logger.error(f"Failed to load model {model_name}: {e}")
@@ -552,7 +553,7 @@ def _load_resources(self) -> None:
552553
else:
553554
# SQLite backend - original behavior
554555
# Load model (shared across all indexes)
555-
if self.indexes and SentenceTransformer:
556+
if self.indexes and SentenceTransformer is not None:
556557
# Get model name from first index
557558
sample_index = next(iter(self.indexes.values()))
558559
model_name = self._get_model_name(sample_index)

tests/unit/ai_chat/test_gateway.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
import json
10-
from collections.abc import AsyncIterator, Awaitable, Callable
10+
from collections.abc import AsyncIterator
1111
from typing import Any
1212

1313
import pytest
@@ -441,10 +441,8 @@ async def test_the_relay_streams_rather_than_collects(
441441
from fastapi.responses import StreamingResponse
442442

443443
client = slow_gateway._client
444-
chunks = []
445444
async with client.raw_post("chat", {"id": "c", "message": "hi"}) as resp:
446-
async for chunk in resp.content.iter_any():
447-
chunks.append(chunk)
445+
chunks = [chunk async for chunk in resp.content.iter_any()]
448446
assert len(chunks) > 1, f"upstream body arrived in one piece: {chunks!r}"
449447
assert chunks[0].strip() == b"", "first chunk should be keepalive padding"
450448

0 commit comments

Comments
 (0)