Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
7b7b35a
fix(ci): multi-OS must use the interpreter setup-python provisioned
mjerris Jul 27, 2026
d8f9966
fix(ci): declare `build` — PACKAGE-SMOKE never had the module it needs
mjerris Jul 27, 2026
95e542b
fix(tests): Windows portability — sqlite handles, path assertions, pe…
mjerris Jul 27, 2026
0f5d0ee
fix(tests): compare UTF-8 round-trip to the template, not a second read
mjerris Jul 27, 2026
330319b
fix(tests): finish the Windows portability tail — path separators, si…
mjerris Jul 27, 2026
bffd790
Merge remote-tracking branch 'origin/fix/multi-os-python-interpreter'…
mjerris Jul 27, 2026
18b056c
Merge remote-tracking branch 'origin/fix/win-test-portability' into HEAD
mjerris Jul 27, 2026
b1ab620
fix(logging): make strip_control_chars a one-parameter public function
mjerris Jul 28, 2026
81d412e
merge: fold PR #77 (Windows/macOS Multi-OS fixes) into the wave-6 set
mjerris Jul 29, 2026
f6ca8c8
ci(gates): schedule CA-VAR, TLS-VERIFY, SECRET-SCRUB and the LEDGER s…
mjerris Jul 29, 2026
7c2f253
fix(security): a secure=True SWAIG tool now REQUIRES a token — absent…
mjerris Jul 29, 2026
f171ce3
fix(datamap): drop `body` from create_simple_api_tool — the key is in…
mjerris Jul 29, 2026
17c301f
chore(doc-surface): re-pin floor 64.7 -> 70.6 after the gate stopped …
mjerris Jul 29, 2026
83a9833
docs(relay): document all 24 from_payload event parsers
mjerris Jul 29, 2026
efbf3cf
docs(rest): document the shared REST base — HTTP verbs + resource CRUD
mjerris Jul 29, 2026
508d75e
docs(search): document the 18 undocumented public symbols in search/
mjerris Jul 29, 2026
3e69e8d
docs(cli): document 26 undocumented public CLI symbols
mjerris Jul 29, 2026
4f43f76
docs(livewire): document the 10 undocumented LiveKit-compat symbols
mjerris Jul 29, 2026
367f3a2
Merge branch 'docburn/search' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
9056363
Merge branch 'docburn/cli' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
c50531b
Merge branch 'docburn/livewire' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
d460ff4
docs: document 18 misc public symbols (ai_chat, relay actions, pom, s…
mjerris Jul 29, 2026
aed6e78
docs(core): document all 17 undocumented public symbols in core/
mjerris Jul 29, 2026
f601032
fix(tests): relay mock probe checked HTTP but the tests connect over WS
mjerris Jul 29, 2026
c28213e
docs(skills): document all 13 undocumented public symbols in skills/
mjerris Jul 29, 2026
ee48fe9
fix(tests): drop the redundant module-level socket import
mjerris Jul 29, 2026
71222a8
Merge branch 'docburn/skills' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
2602581
Merge branch 'docburn/core' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
c2ec6f6
Merge branch 'docburn/misc' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
2850adf
chore(doc-surface): pin the floor at 100% — every public python symbo…
mjerris Jul 29, 2026
8066297
fix(swaig): `str | None` was reported REQUIRED while `Optional[str]` …
mjerris Jul 29, 2026
8ff231f
style(lint): bring tests/, scripts/ and eng/ under the SDK ruleset — …
mjerris Jul 29, 2026
0bb6a1f
style(lint): burn SIM102/103/105/115 + RET504 to zero in tests/
mjerris Jul 29, 2026
88c67f2
style(lint): burn SIM117 to zero in tests/ — collapse nested `with`
mjerris Jul 29, 2026
c9ab46f
style(lint): burn lane-C rules in tests/, scripts/ and eng/ to zero
mjerris Jul 29, 2026
bcdc5b4
Merge branch 'lint/c' into wave6/ctor-dunder-fold
mjerris Jul 29, 2026
08a9e96
fix(tests): restore `import os` — a cross-lane merge interaction drop…
mjerris Jul 29, 2026
520abaa
style(lint): burn F841/RUF059/S108/S107/S110/S603/B017/E712 to zero i…
mjerris Jul 29, 2026
4dd2897
style(lint): burn tests/, scripts/ and eng/ to ZERO — merge lanes A+B+C
mjerris Jul 29, 2026
a23c85b
ci(lint): wire REPO-LINT + REPO-FMT — tests/, scripts/ and eng/ were …
mjerris Jul 29, 2026
69b4e88
test(swml): delete two proven-dead structlog TypeError guards
mjerris Jul 29, 2026
85363f4
fix(tests): mcp_gateway patched os.* while the source uses pathlib (#…
mjerris Jul 29, 2026
4371610
fix(swml): validate post_prompt shape — the validator passed configs …
mjerris Jul 30, 2026
71eed0c
feat(datamap)!: remove DataMap.body() — the builder wrote a key nothi…
mjerris Jul 30, 2026
c0183cd
fix(logging): keep SDK logs off stdout — they were corrupting --dump-…
mjerris Jul 30, 2026
ee1911c
fix(typecheck): put `# type: ignore` back on the line mypy anchors th…
mjerris Jul 30, 2026
e4f66b6
fix(swaig): enforce `secure=True` on all four serverless transports
mjerris Jul 30, 2026
ff8ab26
fix(semver-diff): apply the D5 version-bump hold to python's standalo…
mjerris Jul 30, 2026
27d4fc9
fix(extras): name the extra when an optional dependency is missing
mjerris Jul 30, 2026
c8abfe7
fix(relay): read RELAY_MAX_CONNECTIONS at connect time, not at import
mjerris Jul 30, 2026
e9aa402
fix(skills): make the skip_prompt guard unbypassable
mjerris Jul 30, 2026
57407a7
fix(tls): refuse to start when TLS is requested but unconfigurable
mjerris Jul 30, 2026
0a61040
fix(mcp-gateway): make typing independent of whether the optional ext…
mjerris Jul 30, 2026
381ec5b
fix(skills): native_vector_search contributed no prompt section
mjerris Jul 30, 2026
6ada9a1
test(skills): sweep every registered skill for the skip_prompt contract
mjerris Jul 30, 2026
c5e5516
relay: regenerate protocol_types_generated for calling.conference
mjerris Jul 31, 2026
3c9a6e4
fix(auth): declare the credential params structurally, not as FastAPI…
mjerris Jul 31, 2026
33f0f8d
types(swaig): regenerate against cross-file $ref resolution + re-vend…
mjerris Aug 4, 2026
335c586
ci: pin mypy exact (was an open floor) + a TOOL-PINS gate that proves…
mjerris Aug 4, 2026
295cba0
ci(types): declare types-PyYAML + gate undeclared type stubs (local≠C…
mjerris Aug 4, 2026
4741fb6
regen(swaig): pick up the d8e5787 re-vendor -- four action types tigh…
mjerris Aug 5, 2026
bd6e96a
regen(swaig): pick up the dea604b re-vendor -- one new action, one ne…
mjerris Aug 6, 2026
2eddddd
fix(swml): resolve anyOf/oneOf branches so the closed-key check stays…
mjerris Aug 10, 2026
01c7f4a
docs(schema_utils): correct the false "jsonschema-rs not available" f…
mjerris Aug 10, 2026
13760c3
fix(ci): catch the port_surface_native.json leak with a SURFACE-NATIV…
mjerris Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .doc_surface_floor
Original file line number Diff line number Diff line change
@@ -1 +1 @@
64.7
100.0
19 changes: 17 additions & 2 deletions .github/workflows/doc-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,36 @@ jobs:
# deleted livewire member stops resolving instead of staying excused by a stale
# committed file. Mirrors run-ci.sh's SURFACE-NATIVE gate (DOC-AUDIT deps on it).
- name: Regenerate the native-only doc-audit sidecar
# `python` (the setup-python shim), not bare `python3` — see the PACKAGE-SMOKE
# note in multi-os.yml. This job is ubuntu-only, where setup-python front-loads
# its dir so both names currently resolve to the toolcache; using the shim keeps
# that true if this workflow ever gains a macOS runner, where bare `python3`
# resolves to the framework Python instead.
run: |
python3 signalwire-python/scripts/emit_surface_native.py \
python signalwire-python/scripts/emit_surface_native.py \
--out signalwire-python/port_surface_native.json

- name: Run audit_docs.py against the Python surface
# --native-names is load-bearing: signalwire.livewire.* is excluded from the
# surface oracle by design, but livewire/ docs are in this perimeter, so its
# real members only resolve via the sidecar. See scripts/emit_surface_native.py.
run: |
python3 porting-sdk/scripts/audit_docs.py \
python porting-sdk/scripts/audit_docs.py \
--root signalwire-python \
--surface porting-sdk/python_surface.json \
--ignore signalwire-python/DOC_AUDIT_IGNORE.md \
--native-names signalwire-python/port_surface_native.json

# The DRIFT half of the regenerate/check pair, mirroring run-ci.sh's
# SURFACE-NATIVE-FRESH. Runs AFTER the audit has consumed the fresh file. On CI
# this cannot leak into a commit (the checkout is ephemeral), but it is what
# makes a STALE committed sidecar fail loudly here instead of passing unnoticed
# and then drifting further.
- name: Committed sidecar matches the regenerated one (no drift)
run: |
cd signalwire-python
git diff --exit-code -- port_surface_native.json

- name: Summary
if: always()
run: |
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/multi-os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,16 @@ jobs:
- name: PACKAGE-SMOKE (build + install + import from the built artifact)
shell: bash
working-directory: signalwire-python
run: python3 ../porting-sdk/scripts/package_smoke.py --port python --repo .
# `python`, not `python3`: use the SAME name `pip` above pairs with, so this
# step provably runs the interpreter the deps were installed into. (On the
# macOS/arm64 runner BOTH names resolve to the pre-installed framework Python
# — setup-python does not win PATH there — and `pip` is that Python's pip, so
# the whole job is consistently one interpreter. Pinning the name keeps it that
# way if the image's precedence ever changes.)
#
# The nightly failure (run 30238061313, macos-latest) was
# "No module named build" — NOT an interpreter mismatch: `build` was never a
# declared dependency at all, so the gate silently relied on the runner image
# shipping it. Now declared in requirements-dev.txt, which the step above
# installs. package_smoke.py is not at fault; it correctly uses sys.executable.
run: python ../porting-sdk/scripts/package_smoke.py --port python --repo .
31 changes: 2 additions & 29 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2331,32 +2331,6 @@ data_map.webhook(
)
```

##### `body(data: Dict[str, Any]) -> DataMap`
Set the JSON body for POST/PUT requests.

**Parameters:**
- `data` (Dict[str, Any]): JSON body data (supports `${variable}` substitution)

**Usage:**
```python
# Static body with parameter substitution
data_map.body({
'query': '${args.search_term}',
'limit': 5,
'filters': {
'category': '${args.category}',
'active': True
}
})

# Body with call-related data (NOT sensitive info)
data_map.body({
'customer_id': '${global_data.customer_id}',
'request_id': '${meta_data.call_id}',
'search': '${args.query}'
})
```

##### `params(data: Dict[str, Any]) -> DataMap`
Set URL query parameters.

Expand Down Expand Up @@ -2624,7 +2598,7 @@ search_tool = (DataMap('search_knowledge')
'https://api.company.com/search',
headers={'Authorization': 'Bearer TOKEN'}
)
.body({
.params({
'query': '${args.query}',
'category': '${args.category}',
'limit': 5
Expand Down Expand Up @@ -2695,7 +2669,7 @@ agent.register_swaig_function(swaig_function)

The SDK provides helper functions for common DataMap patterns:

##### `create_simple_api_tool(name: str, url: str, response_template: str, parameters: Optional[Dict[str, Dict]] = None, method: str = "GET", headers: Optional[Dict[str, str]] = None, body: Optional[Dict[str, Any]] = None, error_keys: Optional[List[str]] = None) -> DataMap`
##### `create_simple_api_tool(name: str, url: str, response_template: str, parameters: Optional[Dict[str, Dict]] = None, method: str = "GET", headers: Optional[Dict[str, str]] = None, error_keys: Optional[List[str]] = None) -> DataMap`

Create a simple API integration tool.

Expand All @@ -2706,7 +2680,6 @@ Create a simple API integration tool.
- `parameters` (Optional[Dict[str, Dict]]): Parameter definitions
- `method` (str): HTTP method (default: "GET")
- `headers` (Optional[Dict[str, str]]): HTTP headers
- `body` (Optional[Dict[str, Any]]): Request body
- `error_keys` (Optional[List[str]]): Error field names

**Usage:**
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ All services share the same security configuration options:
}
```

### TLS misconfiguration fails hard

When SSL is enabled (`ssl_enabled: true` / `SWML_SSL_ENABLED=true`) but the
certificate or key is missing or unreadable, the server **refuses to start** and
raises a `RuntimeError`. It does not fall back to plain HTTP.

A silent downgrade would give an operator who asked for encryption a cleartext
listener — carrying, among other things, the Basic-auth credentials — with no
error and nothing in the logs to notice. If you want plain HTTP, disable SSL
explicitly.

## Migration Guide

### From Environment Variables Only
Expand Down
109 changes: 57 additions & 52 deletions eng/validate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,28 @@

Licensed under the MIT License.
See LICENSE file in the project root for full license information.
"""

"""
Validate a JSON file against a JSON Schema.

Usage:
python validate_schema.py <schema_file> <json_file>

Example:
python validate_schema.py schema.json steps3.json
"""

import argparse
import json
import sys
import argparse
from pathlib import Path
from jsonschema import validate, ValidationError, Draft7Validator

from jsonschema import Draft7Validator, ValidationError, validate


def load_json_file(filepath):
"""Load and parse a JSON file."""
try:
with open(filepath, 'r') as f:
with Path(filepath).open() as f:
return json.load(f)
except FileNotFoundError:
print(f"❌ Error: File '{filepath}' not found")
Expand All @@ -43,57 +42,59 @@ def validate_json(schema_file, json_file, verbose=False):
# Load files
print(f"Loading schema from: {schema_file}")
schema = load_json_file(schema_file)

print(f"Loading JSON from: {json_file}")
data = load_json_file(json_file)

# Try to validate
try:
validate(instance=data, schema=schema)
print("\n✅ Validation PASSED!")
return True

except ValidationError as e:
print(f"\n❌ Validation FAILED!")
print("\n❌ Validation FAILED!")
print(f"\nError: {e.message}")

# Show the path where the error occurred
if e.path:
path_str = " -> ".join(str(p) for p in e.path)
print(f"Location: {path_str}")

# Show the failing value if verbose
if verbose and e.instance is not None:
print(f"\nFailing value: {json.dumps(e.instance, indent=2)[:200]}...")

# Show all errors if there are multiple
validator = Draft7Validator(schema)
errors = list(validator.iter_errors(data))

if len(errors) > 1:
print(f"\nFound {len(errors)} validation errors:")
for i, error in enumerate(errors, 1):
print(f"\n{i}. {error.message}")
if error.path:
path_str = " -> ".join(str(p) for p in error.path)
print(f" Location: {path_str}")

# Show schema constraint that failed
if verbose and error.validator:
print(f" Failed constraint: {error.validator}")
if error.validator_value is not None:
print(f" Expected: {json.dumps(error.validator_value, indent=2)[:100]}...")

print(
f" Expected: {json.dumps(error.validator_value, indent=2)[:100]}..."
)

return False

except Exception as e:
print(f"\n❌ Unexpected error: {e}")
return False


def main():
parser = argparse.ArgumentParser(
description='Validate a JSON file against a JSON Schema',
description="Validate a JSON file against a JSON Schema",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Expand All @@ -105,79 +106,83 @@ def main():

# Validate AI config extracted from SWML
python validate_schema.py --extract-ai schema.json steps3.json
"""
""",
)

parser.add_argument('schema', help='Path to the JSON Schema file')
parser.add_argument('json_file', help='Path to the JSON file to validate')
parser.add_argument('-v', '--verbose', action='store_true',
help='Show detailed error information')
parser.add_argument('--extract-ai', action='store_true',
help='Extract and validate just the AI config from SWML format')


parser.add_argument("schema", help="Path to the JSON Schema file")
parser.add_argument("json_file", help="Path to the JSON file to validate")
parser.add_argument(
"-v", "--verbose", action="store_true", help="Show detailed error information"
)
parser.add_argument(
"--extract-ai",
action="store_true",
help="Extract and validate just the AI config from SWML format",
)

args = parser.parse_args()

# Check if files exist
if not Path(args.schema).exists():
print(f"❌ Error: Schema file '{args.schema}' not found")
sys.exit(1)

if not Path(args.json_file).exists():
print(f"❌ Error: JSON file '{args.json_file}' not found")
sys.exit(1)

# Special handling for AI config extraction
if args.extract_ai:
print("Extracting AI config from SWML format...")
data = load_json_file(args.json_file)
schema = load_json_file(args.schema)

# Extract AI config
ai_config = None
if 'sections' in data and 'main' in data.get('sections', {}):
main_section = data.get('sections', {}).get('main', [])
if "sections" in data and "main" in data.get("sections", {}):
main_section = data.get("sections", {}).get("main", [])
for item in main_section:
if isinstance(item, dict) and 'ai' in item:
ai_config = item['ai']
if isinstance(item, dict) and "ai" in item:
ai_config = item["ai"]
break

if not ai_config:
print("❌ Error: No AI config found in SWML format")
sys.exit(1)

# Extract AI schema
ai_schema = None
if '$defs' in schema and 'AI' in schema['$defs']:
ai_def = schema['$defs']['AI']
if 'properties' in ai_def and 'ai' in ai_def['properties']:
ai_schema = ai_def['properties']['ai'].copy()
if "$defs" in schema and "AI" in schema["$defs"]:
ai_def = schema["$defs"]["AI"]
if "properties" in ai_def and "ai" in ai_def["properties"]:
ai_schema = ai_def["properties"]["ai"].copy()
# Create a new schema with only necessary definitions
ai_schema_full = {
"$schema": "http://json-schema.org/draft-07/schema#",
**ai_schema,
"$defs": schema['$defs']
"$defs": schema["$defs"],
}

if not ai_schema:
print("❌ Error: Could not extract AI schema")
sys.exit(1)

# Validate directly without writing temp files
try:
validate(instance=ai_config, schema=ai_schema_full)
print("\n✅ AI config validation PASSED!")
success = True
except ValidationError as e:
print(f"\n❌ AI config validation FAILED!")
print("\n❌ AI config validation FAILED!")
print(f"\nFirst error: {e.message}")
if e.path:
path_str = " -> ".join(str(p) for p in e.path)
print(f"Location: {path_str}")

# Show all errors
validator = Draft7Validator(ai_schema_full)
errors = list(validator.iter_errors(ai_config))

if len(errors) > 1:
print(f"\nFound {len(errors)} validation errors in total:")
for i, error in enumerate(errors, 1):
Expand All @@ -187,14 +192,14 @@ def main():
print(f" Location: {path_str}")
if args.verbose and error.validator:
print(f" Failed constraint: {error.validator}")

success = False
else:
# Normal validation
success = validate_json(args.schema, args.json_file, args.verbose)

sys.exit(0 if success else 1)


if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions examples/data_map_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def setup(self):
"Content-Type": "application/json",
},
)
.body({"query": "${query}", "limit": "${limit}"})
.params({"query": "${query}", "limit": "${limit}"})
.foreach(
{
"input_key": "${response.results}",
Expand Down Expand Up @@ -271,7 +271,7 @@ def print_data_map_examples():
"https://api.docs.com/search",
headers={"Authorization": "Bearer TOKEN"},
)
.body({"query": "${query}", "limit": 3})
.params({"query": "${query}", "limit": 3})
.foreach(
{
"input_key": "${response.results}",
Expand Down
Loading