API Layer¶
Litestar REST + WebSocket API: controllers, authentication, guards, and channels.
App¶
app
¶
Litestar application factory.
Creates and configures the Litestar application with all controllers, middleware, exception handlers, plugins, and lifecycle hooks (startup/shutdown).
create_app
¶
create_app(
*,
config=None,
persistence=None,
message_bus=None,
cost_tracker=None,
approval_store=None,
auth_service=None,
task_engine=None,
coordinator=None,
work_pipeline=None,
intake_entry_adapter=None,
task_board_entry_adapter=None,
agent_registry=None,
meeting_orchestrator=None,
meeting_scheduler=None,
performance_tracker=None,
settings_service=None,
provider_registry=None,
provider_health_tracker=None,
tool_invocation_tracker=None,
delegation_record_store=None,
artifact_storage=None,
audit_log=None,
trust_service=None,
coordination_metrics_store=None,
training_service=None,
event_stream_hub=None,
interrupt_store=None,
client_simulation_state=None,
_skip_lifecycle_shutdown=False,
)
Create and configure the Litestar application.
All parameters are optional for testing -- provide fakes via keyword arguments. Services not explicitly provided are auto-wired from config and environment variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
RootConfig | None
|
Root company configuration. |
None
|
persistence
|
PersistenceBackend | None
|
Persistence backend. |
None
|
message_bus
|
MessageBus | None
|
Internal message bus. |
None
|
cost_tracker
|
CostTracker | None
|
Cost tracking service. |
None
|
approval_store
|
ApprovalStoreProtocol | None
|
Approval queue store. |
None
|
auth_service
|
AuthService | None
|
Pre-built auth service (for testing). |
None
|
task_engine
|
TaskEngine | None
|
Centralized task state engine. |
None
|
coordinator
|
MultiAgentCoordinator | None
|
Multi-agent coordinator. |
None
|
work_pipeline
|
WorkPipeline | None
|
Work pipeline spine (injected double wins over the boot-autowired one). |
None
|
intake_entry_adapter
|
WorkEntryAdapter[Any] | None
|
Real work-entry adapter (injected double wins over the boot-autowired one). |
None
|
task_board_entry_adapter
|
TaskBoardEntryAdapter | None
|
Real task-board work-entry adapter (injected double wins over the boot-autowired one). |
None
|
agent_registry
|
AgentRegistryService | None
|
Agent registry service. |
None
|
meeting_orchestrator
|
MeetingOrchestrator | None
|
Meeting orchestrator. |
None
|
meeting_scheduler
|
MeetingScheduler | None
|
Meeting scheduler. |
None
|
performance_tracker
|
PerformanceTracker | None
|
Performance tracking service. |
None
|
settings_service
|
SettingsService | None
|
Settings service for runtime config. |
None
|
provider_registry
|
ProviderRegistry | None
|
Provider registry. |
None
|
provider_health_tracker
|
ProviderHealthTracker | None
|
Provider health tracking service. |
None
|
tool_invocation_tracker
|
ToolInvocationTracker | None
|
Tool invocation tracking service. |
None
|
delegation_record_store
|
DelegationRecordStore | None
|
Delegation record store. |
None
|
artifact_storage
|
ArtifactStorageBackend | None
|
Artifact storage backend. |
None
|
audit_log
|
AuditLog | None
|
Pre-built audit log (auto-wired if None). |
None
|
trust_service
|
TrustService | None
|
Pre-built trust service. |
None
|
coordination_metrics_store
|
CoordinationMetricsStore | None
|
Pre-built metrics store (auto-wired if None). |
None
|
training_service
|
TrainingService | None
|
Pre-built training service (auto-wired in startup if None and dependencies are available). |
None
|
event_stream_hub
|
EventStreamHub | None
|
Pre-built event stream hub (auto-created if None). |
None
|
interrupt_store
|
InterruptStore | None
|
Pre-built interrupt store (auto-created if None). |
None
|
client_simulation_state
|
ClientSimulationState | None
|
Pre-built client simulation state. Wired before the optional-controller predicate check so the Simulation / Request controllers register correctly on a test app boot. |
None
|
_skip_lifecycle_shutdown
|
bool
|
Test-only flag. When |
False
|
Returns:
| Type | Description |
|---|---|
Litestar
|
Configured Litestar application. |
Source code in src/synthorg/api/app.py
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 | |
Config¶
config
¶
API configuration models.
Frozen Pydantic models for CORS, rate limiting, server,
authentication, and the top-level ApiConfig that aggregates
them all.
CorsConfig
pydantic-model
¶
Bases: BaseModel
CORS configuration for the API.
Attributes:
| Name | Type | Description |
|---|---|---|
allowed_origins |
tuple[str, ...]
|
Origins permitted to make cross-origin requests. |
allow_methods |
tuple[str, ...]
|
HTTP methods permitted in cross-origin requests. |
allow_headers |
tuple[str, ...]
|
Headers permitted in cross-origin requests. |
allow_credentials |
bool
|
Whether credentials (cookies, auth) are allowed in cross-origin requests. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
allowed_origins(tuple[str, ...]) -
allow_methods(tuple[str, ...]) -
allow_headers(tuple[str, ...]) -
allow_credentials(bool)
Validators:
-
_validate_wildcard_credentials
allowed_origins
pydantic-field
¶
Origins permitted to make cross-origin requests
allow_methods
pydantic-field
¶
HTTP methods permitted in cross-origin requests
allow_headers
pydantic-field
¶
Headers permitted in cross-origin requests
allow_credentials
pydantic-field
¶
Whether credentials (cookies) are allowed
RateLimitTimeUnit
¶
Bases: StrEnum
Valid time windows for rate limiting.
RateLimitConfig
pydantic-model
¶
Bases: BaseModel
API rate limiting configuration.
Three tiers stacked around the auth middleware:
- IP floor (outermost, un-gated): keyed by client IP, applies to every request -- including ones the auth middleware rejects with 401. Guards against flood attacks that burn auth-validation cycles on protected endpoints with forged tokens.
- Unauthenticated (middle, only when
scope["user"]isNone): keyed by client IP, aggressive cap on brute-force against login/setup/logout. - Authenticated (innermost, only when
scope["user"]is set): keyed by user ID, generous cap for normal dashboard use.
Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single per-IP budget.
Attributes:
| Name | Type | Description |
|---|---|---|
floor_max_requests |
int
|
Maximum total requests per time window (by IP) across the whole API. Catches traffic that auth_middleware rejects before the unauth tier sees it. |
unauth_max_requests |
int
|
Maximum unauthenticated requests per time window (by IP). |
auth_max_requests |
int
|
Maximum authenticated requests per time window (by user ID). |
time_unit |
RateLimitTimeUnit
|
Time window ( |
exclude_paths |
tuple[str, ...]
|
Paths excluded from rate limiting. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
floor_max_requests(int) -
unauth_max_requests(int) -
auth_max_requests(int) -
time_unit(RateLimitTimeUnit) -
exclude_paths(tuple[str, ...]) -
max_rpm_default(int)
Validators:
-
_validate_floor_above_user_tiers -
_apply_mirrors -
_reject_legacy_max_requests
floor_max_requests
pydantic-field
¶
Maximum total requests per time window (by IP) across the whole API, including requests rejected by the auth middleware. Defense-in-depth against floods of invalid auth attempts on protected endpoints. The floor wraps both user-gated tiers in the middleware stack, so it must be >= auth_max_requests AND >= unauth_max_requests -- a lower floor would silently cap either the authenticated per-user budget or the unauthenticated per-IP budget below its documented value (especially behind a shared NAT where many users share one IP). Enforced by :meth:_validate_floor_above_user_tiers.
unauth_max_requests
pydantic-field
¶
Maximum unauthenticated requests per time window (by IP)
auth_max_requests
pydantic-field
¶
Maximum authenticated requests per time window (by user ID)
exclude_paths
pydantic-field
¶
Paths excluded from rate limiting
max_rpm_default
pydantic-field
¶
Fallback requests-per-minute applied to per-connection coordinators when the catalog does not provide a limiter (mirrors the api.max_rpm_default setting; restart required)
ServerConfig
pydantic-model
¶
Bases: BaseModel
Uvicorn server configuration.
Host, port, TLS paths, trusted-proxy list, and the compression /
request-size limits are resolved at boot via
:func:synthorg.settings.bootstrap_resolver.resolve_init_value
against the api.* registry entries rather than carried on this
model. Only the worker-process / auto-reload / WebSocket-ping knobs
that uvicorn needs at construction time live here.
Attributes:
| Name | Type | Description |
|---|---|---|
reload |
bool
|
Enable auto-reload for development. |
workers |
int
|
Number of worker processes. |
ws_ping_interval |
float
|
WebSocket ping interval in seconds (0 to disable). |
ws_ping_timeout |
float
|
WebSocket pong timeout in seconds. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
reload(bool) -
workers(int) -
ws_ping_interval(float) -
ws_ping_timeout(float)
ws_ping_interval
pydantic-field
¶
WebSocket ping interval in seconds (0 to disable)
ApiConfig
pydantic-model
¶
Bases: BaseModel
Top-level API configuration aggregating all sub-configs.
Attributes:
| Name | Type | Description |
|---|---|---|
cors |
CorsConfig
|
CORS configuration. |
rate_limit |
RateLimitConfig
|
Global three-tier rate limiting configuration (IP floor un-gated, unauthenticated by IP, authenticated by user ID). |
rate_limiter_enabled |
bool
|
Master kill switch for the three-tier
global rate limiter. Mirrors the
|
per_op_rate_limit |
PerOpRateLimitConfig
|
Per-operation throttling configuration (layered on top of the global three-tier limiter). |
per_op_concurrency |
PerOpConcurrencyConfig
|
Per-operation inflight concurrency capping (layered on top of the sliding-window per-op limiter; caps simultaneous long-running requests per operation per subject). |
server |
ServerConfig
|
Uvicorn server configuration. |
auth |
AuthConfig
|
Authentication configuration. |
api_prefix |
NotBlankStr
|
URL prefix for all API routes. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
cors(CorsConfig) -
rate_limit(RateLimitConfig) -
rate_limiter_enabled(bool) -
per_op_rate_limit(PerOpRateLimitConfig) -
per_op_concurrency(PerOpConcurrencyConfig) -
server(ServerConfig) -
auth(AuthConfig) -
api_prefix(NotBlankStr)
Validators:
-
_apply_mirrors
rate_limit
pydantic-field
¶
Global three-tier rate limiting configuration: un-gated IP floor, unauthenticated by IP, authenticated by user ID
rate_limiter_enabled
pydantic-field
¶
Master kill switch for the three-tier global rate limiter. Mirrors the api.rate_limiter_enabled registry entry (read_only_post_init=True): the boot-time resolver in api/app.py reads SYNTHORG_API_RATE_LIMITER_ENABLED and falls through to the registered default (env > code default per the Cat-2 precedence model).
per_op_rate_limit
pydantic-field
¶
Per-operation throttling (layered on the global limiter)
per_op_concurrency
pydantic-field
¶
Per-operation inflight concurrency capping (layered on the sliding-window per-op limiter; caps simultaneous long-running requests per (operation, subject))
DTOs¶
dto
¶
Request/response DTOs and envelope models.
Response envelopes wrap all API responses in a consistent structure. Request DTOs define write-operation payloads (separate from domain models because they omit server-generated fields).
ErrorDetail
pydantic-model
¶
Bases: BaseModel
Structured error metadata (RFC 9457).
Self-contained so agents can parse it without referencing the parent envelope.
Attributes:
| Name | Type | Description |
|---|---|---|
detail |
NotBlankStr
|
Human-readable occurrence-specific explanation. |
error_code |
ErrorCode
|
Machine-readable error code (by convention, 4-digit
category-grouped; see |
error_category |
ErrorCategory
|
High-level error category. |
retryable |
bool
|
Whether the client should retry the request. |
retry_after |
int | None
|
Seconds to wait before retrying ( |
instance |
NotBlankStr
|
Request correlation ID for log tracing. |
title |
NotBlankStr
|
Static per-category title (e.g. "Authentication Error"). |
type |
NotBlankStr
|
Documentation URI for the error category. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
detail(NotBlankStr) -
error_code(ErrorCode) -
error_category(ErrorCategory) -
retryable(bool) -
retry_after(int | None) -
instance(NotBlankStr) -
title(NotBlankStr) -
type(NotBlankStr)
Validators:
-
_validate_retry_after_consistency
retry_after
pydantic-field
¶
Seconds to wait before retrying (null when not applicable).
ProblemDetail
pydantic-model
¶
Bases: BaseModel
Bare RFC 9457 application/problem+json response body.
Returned when the client sends Accept: application/problem+json.
Attributes:
| Name | Type | Description |
|---|---|---|
type |
NotBlankStr
|
Documentation URI for the error category. |
title |
NotBlankStr
|
Static per-category title. |
status |
int
|
HTTP status code. |
detail |
NotBlankStr
|
Human-readable occurrence-specific explanation. |
instance |
NotBlankStr
|
Request correlation ID for log tracing. |
error_code |
ErrorCode
|
Machine-readable 4-digit error code. |
error_category |
ErrorCategory
|
High-level error category. |
retryable |
bool
|
Whether the client should retry the request. |
retry_after |
int | None
|
Seconds to wait before retrying ( |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
type(NotBlankStr) -
title(NotBlankStr) -
status(int) -
detail(NotBlankStr) -
instance(NotBlankStr) -
error_code(ErrorCode) -
error_category(ErrorCategory) -
retryable(bool) -
retry_after(int | None)
Validators:
-
_validate_retry_after_consistency
retry_after
pydantic-field
¶
Seconds to wait before retrying (null when not applicable).
ApiResponse
pydantic-model
¶
Bases: BaseModel
Standard API response envelope.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
T | None
|
Response payload ( |
error |
str | None
|
Operator-facing error message ( |
error_detail |
ErrorDetail | None
|
Structured error metadata ( |
success |
bool
|
Whether the request succeeded (computed from |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
data(T | None) -
error(str | None) -
error_detail(ErrorDetail | None)
Validators:
-
_validate_error_detail_consistency
success
property
¶
Whether the request succeeded (derived from error).
Returns:
| Type | Description |
|---|---|
bool
|
|
PaginationMeta
pydantic-model
¶
Bases: BaseModel
Pagination metadata for list responses.
Cursor-based: clients receive an opaque next_cursor and walk
forward until has_more is False.
Attributes:
| Name | Type | Description |
|---|---|---|
limit |
int
|
Maximum items per page. |
next_cursor |
str | None
|
Opaque cursor for the next page ( |
has_more |
bool
|
Whether more items follow the current page. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
limit(int) -
next_cursor(str | None) -
has_more(bool)
Validators:
-
_validate_cursor_consistency
PaginatedResponse
pydantic-model
¶
Bases: BaseModel
Paginated API response envelope.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
tuple[T, ...]
|
Page of items. |
error |
str | None
|
Error message ( |
error_detail |
ErrorDetail | None
|
Structured error metadata ( |
pagination |
PaginationMeta
|
Pagination metadata. |
degraded_sources |
tuple[NotBlankStr, ...]
|
Data sources that failed gracefully, resulting in partial data. Empty when all sources responded normally. |
success |
bool
|
Whether the request succeeded (computed from |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
data(tuple[T, ...]) -
error(str | None) -
error_detail(ErrorDetail | None) -
pagination(PaginationMeta) -
degraded_sources(tuple[NotBlankStr, ...])
Validators:
-
_validate_error_detail_consistency
CreateArtifactRequest
pydantic-model
¶
Bases: BaseModel
Payload for creating a new artifact.
Attributes:
| Name | Type | Description |
|---|---|---|
type |
ArtifactType
|
Artifact type (code, tests, documentation). |
path |
NotBlankStr
|
Logical file/directory path of the artifact. |
task_id |
NotBlankStr
|
ID of the originating task. |
created_by |
NotBlankStr
|
Agent ID of the creator. |
description |
str
|
Human-readable description. |
content_type |
str
|
MIME content type (empty if no content stored). |
project_id |
NotBlankStr | None
|
Optional project ID to link the artifact to. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
type(ArtifactType) -
path(NotBlankStr) -
task_id(NotBlankStr) -
created_by(NotBlankStr) -
description(str) -
content_type(str) -
project_id(NotBlankStr | None)
content_type
pydantic-field
¶
MIME type of the artifact content (empty when no content is stored).
CreateProjectRequest
pydantic-model
¶
Bases: BaseModel
Payload for creating a new project.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
NotBlankStr
|
Project display name. |
description |
str
|
Detailed project description. |
team |
tuple[NotBlankStr, ...]
|
Agent IDs assigned to the project. |
lead |
NotBlankStr | None
|
Agent ID of the project lead. |
deadline |
str | None
|
Optional deadline (ISO 8601 string). |
budget |
float
|
Total budget in base currency. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
name(NotBlankStr) -
description(str) -
team(tuple[NotBlankStr, ...]) -
lead(NotBlankStr | None) -
deadline(str | None) -
budget(float)
Validators:
-
_validate_request
CreateTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for creating a new task.
Attributes:
| Name | Type | Description |
|---|---|---|
title |
NotBlankStr
|
Short task title. |
description |
NotBlankStr
|
Detailed task description. |
type |
TaskType
|
Task work type. |
priority |
Priority
|
Task priority level. |
project |
NotBlankStr
|
Project ID. |
created_by |
NotBlankStr
|
Agent name of the creator. |
assigned_to |
NotBlankStr | None
|
Optional assignee agent ID. |
estimated_complexity |
Complexity
|
Complexity estimate. |
budget_limit |
float
|
Maximum spend in base currency. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
title(NotBlankStr) -
description(NotBlankStr) -
type(TaskType) -
priority(Priority) -
project(NotBlankStr) -
created_by(NotBlankStr) -
assigned_to(NotBlankStr | None) -
estimated_complexity(Complexity) -
budget_limit(float)
UpdateTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for updating task fields.
All fields are optional -- only provided fields are updated.
Attributes:
| Name | Type | Description |
|---|---|---|
title |
NotBlankStr | None
|
New title. |
description |
NotBlankStr | None
|
New description. |
priority |
Priority | None
|
New priority. |
assigned_to |
NotBlankStr | None
|
New assignee. |
budget_limit |
float | None
|
New budget limit. |
expected_version |
int | None
|
Optimistic concurrency guard. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
title(NotBlankStr | None) -
description(NotBlankStr | None) -
priority(Priority | None) -
assigned_to(NotBlankStr | None) -
budget_limit(float | None) -
expected_version(int | None)
TransitionTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for a task status transition.
Attributes:
| Name | Type | Description |
|---|---|---|
target_status |
TaskStatus
|
The desired target status. |
assigned_to |
NotBlankStr | None
|
Optional assignee override for the transition. |
expected_version |
int | None
|
Optimistic concurrency guard. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
target_status(TaskStatus) -
assigned_to(NotBlankStr | None) -
expected_version(int | None)
RegisterExperimentVariantRequest
pydantic-model
¶
Bases: BaseModel
Payload for registering an A/B experiment variant.
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
variant(NotBlankStr) -
weight(int) -
description(str)
AssignExperimentRequest
pydantic-model
¶
Bases: BaseModel
Payload for requesting a deterministic variant assignment.
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
ExecuteTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for the worker-callable POST /tasks/{id}/execute endpoint.
Mirrors the TaskClaim envelope fields the worker carries so the
backend's WorkerExecutionService has the same provenance the
dispatcher captured when it built the claim. The endpoint only
needs the status pair and the dedup key; the task body is read
server-side via the task repository.
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
TaskBoardSubmissionResponse
pydantic-model
¶
Bases: BaseModel
Acknowledgement envelope for POST /tasks (HTTP 202 Accepted).
The board hands the filing to the work-entry adapter; the adapter
drives the pipeline spine in a detached background coroutine. The
spine creates the task during its intake phase, so this response
carries the correlation id rather than a task id: the board UI
correlates the eventual task.created WS event by this id.
Attributes:
| Name | Type | Description |
|---|---|---|
correlation_id |
NotBlankStr
|
End-to-end trace id stamped onto the work item. |
title |
NotBlankStr
|
Title submitted by the user (echoed for UX confirmation). |
project |
NotBlankStr
|
Project the task was filed into. |
status |
Literal['submitted']
|
Always |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
correlation_id(NotBlankStr) -
title(NotBlankStr) -
project(NotBlankStr) -
status(Literal['submitted'])
CancelTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for cancelling a task.
Attributes:
| Name | Type | Description |
|---|---|---|
reason |
NotBlankStr
|
Reason for cancellation. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
CreateApprovalRequest
pydantic-model
¶
Bases: BaseModel
Payload for creating a new approval item.
Attributes:
| Name | Type | Description |
|---|---|---|
action_type |
NotBlankStr
|
Kind of action requiring approval
( |
title |
NotBlankStr
|
Short summary. |
description |
NotBlankStr
|
Detailed explanation. |
risk_level |
ApprovalRiskLevel
|
Assessed risk level. |
ttl_seconds |
int | None
|
Optional time-to-live in seconds (min 60, max 604 800 = 7 days). |
task_id |
NotBlankStr | None
|
Optional associated task. |
metadata |
dict[str, str]
|
Additional key-value pairs. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
action_type(NotBlankStr) -
title(NotBlankStr) -
description(NotBlankStr) -
risk_level(ApprovalRiskLevel) -
ttl_seconds(int | None) -
task_id(NotBlankStr | None) -
metadata(dict[str, str])
Validators:
-
_validate_action_type_format→action_type -
_validate_metadata_bounds
action_type
pydantic-field
¶
Kind of action requiring approval in category:action format.
description
pydantic-field
¶
Detailed explanation of the action and why it requires approval.
ttl_seconds
pydantic-field
¶
Optional time-to-live in seconds before the approval auto-expires (minimum 60, maximum 604800 = 7 days).
ApproveRequest
pydantic-model
¶
Bases: BaseModel
Payload for approving an approval item.
Attributes:
| Name | Type | Description |
|---|---|---|
comment |
NotBlankStr | None
|
Optional comment explaining the approval. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
comment(NotBlankStr | None)
RejectRequest
pydantic-model
¶
Bases: BaseModel
Payload for rejecting an approval item.
Attributes:
| Name | Type | Description |
|---|---|---|
reason |
NotBlankStr
|
Mandatory reason for rejection. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
reason(NotBlankStr)
CoordinateTaskRequest
pydantic-model
¶
Bases: BaseModel
Payload for triggering multi-agent coordination on a task.
Attributes:
| Name | Type | Description |
|---|---|---|
agent_names |
tuple[NotBlankStr, ...] | None
|
Agent names to coordinate with ( |
max_subtasks |
int
|
Maximum subtasks for decomposition. |
max_concurrency_per_wave |
int | None
|
Override for max concurrency per wave. |
fail_fast |
bool | None
|
Override for fail-fast behaviour ( |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
agent_names(tuple[NotBlankStr, ...] | None) -
max_subtasks(int) -
max_concurrency_per_wave(int | None) -
fail_fast(bool | None)
Validators:
-
_validate_unique_agent_names
CoordinationPhaseResponse
pydantic-model
¶
Bases: BaseModel
Response model for a single coordination phase.
Attributes:
| Name | Type | Description |
|---|---|---|
phase |
NotBlankStr
|
Phase name. |
success |
bool
|
Whether the phase completed successfully. |
duration_seconds |
float
|
Wall-clock duration of the phase. |
error |
NotBlankStr | None
|
Error description if the phase failed. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
phase(NotBlankStr) -
success(bool) -
duration_seconds(float) -
error(NotBlankStr | None)
Validators:
-
_validate_success_error_consistency
CoordinationResultResponse
pydantic-model
¶
Bases: BaseModel
Response model for a complete coordination run.
Attributes:
| Name | Type | Description |
|---|---|---|
parent_task_id |
NotBlankStr
|
ID of the parent task. |
topology |
NotBlankStr
|
Resolved coordination topology. |
total_duration_seconds |
float
|
Total wall-clock duration. |
total_cost |
float
|
Total cost across all waves. |
phases |
tuple[CoordinationPhaseResponse, ...]
|
Phase results in execution order. |
wave_count |
int
|
Number of execution waves. |
is_success |
bool
|
Whether all phases succeeded (computed). |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
parent_task_id(NotBlankStr) -
topology(NotBlankStr) -
total_duration_seconds(float) -
total_cost(float) -
currency(str) -
phases(tuple[CoordinationPhaseResponse, ...]) -
wave_count(int)
RollbackAgentIdentityRequest
pydantic-model
¶
Bases: BaseModel
Request body for rolling back an agent identity to a previous version.
Attributes:
| Name | Type | Description |
|---|---|---|
target_version |
int
|
Snapshot version number to restore content from (monotonic counter in the agent_identity_versions table). |
reason |
NotBlankStr | None
|
Optional human-readable justification recorded alongside the evolution event for audit purposes. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
target_version(int) -
reason(NotBlankStr | None)
Errors¶
The error taxonomy and exception classes live in
synthorg.core:
synthorg.core.error_taxonomy--ErrorCategory,ErrorCode, RFC 9457 helperssynthorg.core.domain_errors--DomainErrorbase + concrete subclasses (NotFoundError,ConflictError,ValidationError, ...)synthorg.core.persistence_errors--PersistenceErrorhierarchy
Guards¶
guards
¶
Route guards for access control.
Guards read the authenticated user identity from connection.user
(populated by the auth middleware) and check role-based permissions.
The require_roles factory creates guards for arbitrary role sets.
Pre-built constants cover common patterns::
require_ceo -- CEO only
require_ceo_or_manager -- CEO or Manager
require_approval_roles -- CEO, Manager, or Board Member
require_ceo_or_manager
module-attribute
¶
require_ceo_or_manager = require_roles(CEO, MANAGER)
Guard allowing CEO or Manager roles.
require_approval_roles
module-attribute
¶
require_approval_roles = require_roles(CEO, MANAGER, BOARD_MEMBER)
Guard allowing roles that can approve or reject actions.
has_write_role
¶
Return True if the role grants write access.
Use this for inline role checks instead of importing _WRITE_ROLES
directly. The write set includes CEO, Manager, and Pair Programmer.
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in src/synthorg/api/guards.py
require_write_access
¶
Guard that allows only write-capable human roles.
Checks connection.user.role for ceo, manager,
or pair_programmer. Board members are excluded (they
may only observe and approve). The system role is
intentionally excluded -- use require_roles() with the
desired roles for endpoints the CLI needs to reach.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
ASGIConnection
|
The incoming connection. |
required |
_
|
object
|
Route handler (unused). |
required |
Raises:
| Type | Description |
|---|---|
PermissionDeniedException
|
If the role is not permitted. |
Source code in src/synthorg/api/guards.py
require_read_access
¶
Guard that allows all human roles (excludes SYSTEM).
Checks connection.user.role for any human role
including observer and board_member. The internal
system role is excluded -- use require_roles() for
endpoints the CLI needs to reach.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
ASGIConnection
|
The incoming connection. |
required |
_
|
object
|
Route handler (unused). |
required |
Raises:
| Type | Description |
|---|---|
PermissionDeniedException
|
If the role is not permitted. |
Source code in src/synthorg/api/guards.py
require_roles
¶
Create a guard that allows only the specified roles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*roles
|
HumanRole
|
One or more |
()
|
Returns:
| Type | Description |
|---|---|
Callable[[ASGIConnection, object], None]
|
A guard function compatible with Litestar's guard protocol. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no roles are provided. |
Source code in src/synthorg/api/guards.py
require_org_mutation
¶
Guard factory for org config mutations.
Access is granted if the user has one of:
OrgRole.OWNER-- always allowedOrgRole.EDITOR-- always allowedOrgRole.DEPARTMENT_ADMIN-- allowed only when the target department (read from the path parameter named department_param) is in the user'sscoped_departments
If the user has no org_roles (empty tuple), falls back to
the existing HumanRole write-access check so legacy
installations without organisation-level roles still resolve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
department_param
|
str | None
|
Path parameter name containing the target
department (e.g. |
None
|
Returns:
| Type | Description |
|---|---|
Callable[[ASGIConnection, object], None]
|
A guard function compatible with Litestar's guard protocol. |
Raises:
| Type | Description |
|---|---|
PermissionDeniedException
|
Raised on the corresponding failure path. |
Source code in src/synthorg/api/guards.py
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | |
Middleware¶
middleware
¶
Request middleware and before-send hooks.
Provides ASGI middleware for request logging, and a before_send
hook that injects security headers (CSP, CORP, HSTS, Cache-Control,
etc.) into every HTTP response -- including exception-handler and
unmatched-route (404/405) responses.
Why before_send instead of ASGI middleware?
Litestar's before_send hook wraps the ASGI send callback at
the outermost layer (before the middleware stack), so it fires for
all responses. By contrast, user-defined ASGI middleware only runs
for matched routes -- 404 and 405 responses from the router bypass it.
RequestLoggingMiddleware
¶
ASGI middleware that logs request start and completion.
Uses time.perf_counter() for high-resolution duration
measurement. Only logs HTTP requests (non-HTTP scopes like
WebSocket and lifespan are passed through without logging).
Each HTTP request is also wrapped in an OpenTelemetry span
(http.request) carrying OTel-semconv attributes
(http.request.method, http.route,
http.response.status_code) plus the synthorg.correlation_id
so distributed traces line up with the structured-log stream. When
no tracer provider is configured (default), get_tracer returns
a no-op tracer and the span is essentially free.
Source code in src/synthorg/api/middleware.py
__call__
async
¶
Process an ASGI request, logging start and completion.
Source code in src/synthorg/api/middleware.py
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | |
build_docs_csp
¶
Build the relaxed Scalar UI CSP from a list of trusted origins.
Origins are applied uniformly to script-src, style-src,
img-src, font-src and connect-src so operators can
swap the public Scalar hosts for an internally-mirrored CDN with
a single configuration change.
An empty origins list raises ValueError rather than emit a
malformed CSP with trailing whitespace before each ;. CSP
parsers tolerate the trailing space but operators reading the
header back would see an obviously broken policy; the
ApiBridgeConfig validator is the right place to enforce
non-empty (currently only validates pattern), so callers pass
through the bridge-config-validated tuple.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origins
|
Sequence[str]
|
Origin URLs that Scalar UI assets and proxy requests
may target. Must be non-empty. Each entry must already be
a valid origin (scheme + host); |
required |
Returns:
| Type | Description |
|---|---|
str
|
A CSP header value safe to assign to |
str
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If origins is empty. |
Source code in src/synthorg/api/middleware.py
set_docs_csp_origins
¶
Replace the docs CSP value with one built from origins.
Called once at app startup after resolving
api.csp_docs_external_origins through the settings service.
Reset to the default list with _DOCS_CSP_DEFAULT_ORIGINS for
test isolation.
Calling this outside startup creates a brief eventual-consistency
window for in-flight HTTP responses, since the docs before_send
hook reads the global at request time. The
api.csp_docs_external_origins setting is marked
restart_required=True precisely to keep this single-writer.
Source code in src/synthorg/api/middleware.py
security_headers_hook
async
¶
Inject security headers into every HTTP response.
Registered as a Litestar before_send hook so it fires for
all HTTP responses -- successful, exception-handler, and
router-level 404/405.
Adds static security headers (CORP, HSTS, X-Content-Type-Options,
etc.) and path-aware Content-Security-Policy (strict for API,
relaxed for /docs/ to allow Scalar UI resources) and
Cache-Control (no-store for API, public, max-age=300
for /docs/ since it serves public, non-user-specific content).
Uses __setitem__ (not add) so that if any handler or
middleware already set a header, the known-good value overwrites
it rather than creating a duplicate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
Message
|
ASGI message dict (only |
required |
scope
|
Scope
|
ASGI connection scope. |
required |
Source code in src/synthorg/api/middleware.py
Pagination¶
pagination
¶
Cursor-based pagination helpers.
In-memory helper :func:paginate_cursor slices a tuple and produces a
signed cursor so controllers backed by in-memory collections (config
lists, bus channel names, approval-store filtered views) can return
the same envelope shape as repo-backed endpoints.
The cursor layer is opaque offset encoding today. Repositories that
need seek-based paging (append-only tables) decode the opaque cursor
into a composite (created_at, id) seek tuple internally -- the
wire format stays the same.
CursorLimit
module-attribute
¶
CursorLimit = Annotated[
int,
QueryParameter(
ge=1,
le=MAX_LIMIT,
description=f"Page size (default {DEFAULT_LIMIT}, max {MAX_LIMIT})",
),
]
Query-parameter type for the page size (1-MAX_LIMIT).
HTTP-boundary only: the bounds are enforced by Litestar's
QueryParameter metadata at request parsing. Do not reuse this
alias for in-process validation, where the constraint would silently
not apply.
CursorParam
module-attribute
¶
CursorParam = Annotated[
str | None,
QueryParameter(
max_length=512,
description="Opaque pagination cursor returned by the previous page",
),
]
Query-parameter type for the opaque cursor (max 512 chars).
HTTP-boundary only: the max_length is enforced by Litestar's
QueryParameter metadata at request parsing, not by the type
itself. Do not reuse this alias for in-process validation.
InvalidCursorError
¶
Bases: ValidationError
Raised when a cursor token is malformed, tampered, or unsigned.
Renders as HTTP 422 Unprocessable Entity with a structured
ErrorDetail (error_category=validation,
error_code=VALIDATION_ERROR) via the centralised RFC 9457
dispatch.
Source code in src/synthorg/core/domain_errors.py
paginate_cursor
¶
Slice a tuple and produce cursor-based pagination metadata.
Clamps limit to [1, MAX_LIMIT]. A missing cursor starts at
offset 0. Invalid / tampered cursors raise :class:InvalidCursorError
which controllers should surface as HTTP 400.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
tuple[T, ...]
|
Full collection to paginate (must be already ordered). |
required |
limit
|
int
|
Maximum items to return on this page. |
required |
cursor
|
str | None
|
Opaque cursor from the previous page, or |
required |
secret
|
CursorSecret
|
HMAC secret used to sign / verify cursors. |
required |
Returns:
| Type | Description |
|---|---|
tuple[tuple[T, ...], PaginationMeta]
|
Tuple of (page_items, pagination_meta). |
Raises:
| Type | Description |
|---|---|
InvalidCursorError
|
If |
Source code in src/synthorg/api/pagination.py
encode_repo_seek_meta
¶
Build PaginationMeta for controllers that push limit+offset into the repo.
Centralizes the has_more snapshot-drift guard so the next
pagination bug cannot regress across every version-history
controller one at a time. An empty or short page (page_len ==
0 or offset + page_len == offset) cannot advance the cursor
past the current offset, so the guard refuses to emit a cursor
that would loop the client on the same page when
count_versions disagrees with list_versions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offset
|
int
|
The decoded cursor offset the current page started at. |
required |
page_len
|
int
|
The number of repo rows consumed ( |
required |
total
|
int
|
The repo's reported total row count. Drives the
|
required |
limit
|
int
|
The page size requested. |
required |
secret
|
CursorSecret
|
HMAC secret used to sign the |
required |
reject_stale_cursor
|
bool
|
When |
True
|
Returns:
| Type | Description |
|---|---|
PaginationMeta
|
|
PaginationMeta
|
fields filled in, safe to wrap in |
Raises:
| Type | Description |
|---|---|
InvalidCursorError
|
When the cursor's decoded offset is past
the repo end. |
Source code in src/synthorg/api/pagination.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
encode_countless_seek_meta
¶
Build PaginationMeta for repos that skip the COUNT(*) round-trip.
Counterpart to :func:encode_repo_seek_meta for endpoints that
use the fetch limit+1, detect overflow pattern instead of
issuing a separate count query. The caller fetches up to
limit + 1 rows from the backing store; this helper uses the
overflow to drive has_more and ensures PaginationMeta.total
stays None so clients know the count is unknown (and must
derive display counts from data.length per the frontend
contract in web/CLAUDE.md).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offset
|
int
|
The decoded cursor offset the current page started at. |
required |
fetched_rows
|
int
|
The number of rows the repo returned when asked
for |
required |
limit
|
int
|
The page size requested. |
required |
secret
|
CursorSecret
|
HMAC secret used to sign the |
required |
Returns:
| Type | Description |
|---|---|
PaginationMeta
|
|
PaginationMeta
|
|
Raises:
| Type | Description |
|---|---|
InvalidCursorError
|
When |
Source code in src/synthorg/api/pagination.py
encode_keyset_meta
¶
Build PaginationMeta for a keyset-paginated read.
Keyset pagination is stable under concurrent inserts and deletes:
the cursor encodes the sort key of the last row returned, and the
next page reads WHERE sort_key > after_key. Out-of-bounds
cursors degrade gracefully -- a cursor pointing past the current
end of the collection just returns an empty page (rather than the
offset-pagination InvalidCursorError for offset > total)
because keyset reads cannot tell whether a cursor is "stale" or
just pointing at a row that has been deleted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
next_after_key
|
str | None
|
Sort key of the last row on the page that was
just returned, or |
required |
has_more
|
bool
|
Whether the caller observed an overflow row when
fetching |
required |
limit
|
int
|
Page size requested. |
required |
secret
|
CursorSecret
|
HMAC secret used to sign the |
required |
Returns:
| Type | Description |
|---|---|
PaginationMeta
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/synthorg/api/pagination.py
WebSocket Models¶
ws_models
¶
WebSocket event models for real-time feeds.
Defines event types and the WsEvent payload that is
serialised to JSON and pushed to WebSocket subscribers.
WsEventType
¶
Bases: StrEnum
Types of real-time WebSocket events.
WsEvent
pydantic-model
¶
Bases: BaseModel
A real-time event pushed over WebSocket.
Callers must not mutate the payload dict after construction;
the dict is a mutable reference inside a frozen model.
Attributes:
| Name | Type | Description |
|---|---|---|
version |
int
|
Wire-protocol version. Clients MUST ignore events whose
version they do not understand. Bump only when introducing a
breaking change to |
event_type |
WsEventType
|
Classification of the event. |
channel |
NotBlankStr
|
Target channel name. |
timestamp |
AwareDatetime
|
When the event occurred. |
payload |
dict[str, object]
|
Event-specific data. |
Config:
frozen:Trueallow_inf_nan:Falseextra:forbid
Fields:
-
version(int) -
event_type(WsEventType) -
channel(NotBlankStr) -
timestamp(AwareDatetime) -
payload(dict[str, object])
Validators:
-
_deep_copy_payload -
_validate_payload_shape
version
pydantic-field
¶
WS wire-protocol version (clients ignore unknown)
Auth¶
The auth domain types (AuthConfig, User, ApiKey,
AuthenticatedUser, OrgRole, HumanRole, Session,
RefreshRecord) live under
synthorg.core.auth; the HTTP-coupled
service, middleware, and request-scoped user binding live in
synthorg.api.auth.
AuthContextMiddleware (in synthorg.api.auth.context) runs
immediately after ApiAuthMiddleware and binds the authenticated
user into a per-asyncio-Task ContextVar, so controllers and
audit helpers read the user via no-argument accessors
(get_authenticated_user_id, get_authenticated_user,
audit_actor_from_context) without threading a Request.
service
¶
Authentication service -- password hashing, JWT ops, API key hashing.
SecretNotConfiguredError
¶
RefreshRotation
pydantic-model
¶
Bases: BaseModel
Result of a successful refresh-token rotation.
The controller turns this into the session/csrf/refresh cookies
and emits the post-persistence SECURITY_AUTH_REFRESH_CONSUMED
audit event. session_id is the original session id (the
access token rotated in place), not a freshly minted one.
Config:
frozen:Trueextra:forbid
Fields:
-
token(str) -
expires_in(int) -
session_id(str) -
user(User)
AuthService
¶
Immutable authentication operations.
Owns the cryptographic primitives behind login: Argon2id password hashing and verification, JWT mint and decode, HMAC-SHA256 API key hashing, secure API key generation, and refresh-token persistence through the auth-domain boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AuthConfig
|
Authentication configuration (carries JWT secret). |
required |
Async vs sync. Methods follow a single rule: an operation is
declared async only when it touches an event-loop boundary --
either offloading CPU-bound work via :func:asyncio.to_thread,
or awaiting a repository write. Everything else stays sync.
- :meth:
hash_password_asyncand :meth:verify_password_asyncare async because Argon2id is CPU-bound (3 time-cost iterations over 64MiB of memory by default); :func:asyncio.to_threadkeeps a single login from stalling every concurrent request waiting on the loop. - :meth:
persist_refresh_tokenis async because it awaits a repository write through the auth-domain boundary. - :meth:
create_token, :meth:decode_token, :meth:hash_api_key, and :meth:generate_api_keyare sync: each is either pure CPU with bounded sub-millisecond cost (HMAC,secrets.token_urlsafe) or an in-process JWT codec call with no I/O.
Thread-safety. Instances are safe to share across the
request-handler pool without external locking. After
:meth:__init__, the only state held is _config: AuthConfig
-- itself a Pydantic frozen=True model. The module-global
:class:argon2.PasswordHasher is configured once at import and
treated as a deployment-wide concern (Argon2 parameter selection
is not per-request); the underlying argon2 and jwt
libraries are stateless and thread-safe.
Out of scope. This service does not implement token
revocation (the auth middleware enforces that by checking
pwd_sig on every request), session storage (handled by the
refresh-token repository), or SYSTEM-role token minting
(rejected by :meth:create_token; SYSTEM tokens are minted by
the Go CLI with :data:SYSTEM_ISSUER / :data:SYSTEM_AUDIENCE).
Source code in src/synthorg/api/auth/service.py
hash_password_async
async
¶
Hash a password with Argon2id off the event loop.
Argon2id is CPU-bound; asyncio.to_thread defers the work
to the default thread pool so a single login request cannot
stall every concurrent request waiting on the loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
password
|
str
|
Plaintext password. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Argon2id hash string. |
Source code in src/synthorg/api/auth/service.py
verify_password_async
async
¶
Verify a password against an Argon2id hash off the event loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
password
|
str
|
Plaintext password to check. |
required |
password_hash
|
str
|
Stored Argon2id hash. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
VerificationError
|
On non-mismatch verification failures (e.g. unsupported parameters). |
InvalidHashError
|
If the stored hash is corrupted or malformed (data integrity issue). |
Source code in src/synthorg/api/auth/service.py
create_token
¶
Create a JWT for the given human user.
The token includes a pwd_sig claim -- a 16-character
truncated SHA-256 of the stored password hash. This is
plain SHA-256, not HMAC -- the password hash is already a
high-entropy Argon2id output, and the claim is protected
by the JWT signature. The auth middleware validates this
claim on every request so that tokens issued before a
password change are automatically rejected.
A jti (JWT ID) claim is included for per-token session
tracking and revocation.
SYSTEM-role tokens are minted by the Go CLI with
:data:SYSTEM_ISSUER / :data:SYSTEM_AUDIENCE -- never by
this method. Calling create_token with a SYSTEM user
would mint a token bearing :data:USER_ISSUER /
:data:USER_AUDIENCE, which the middleware's
_resolve_jwt_user immediately rejects (per-role iss/aud
enforcement). We fail-fast with ValueError here so a
future caller that accidentally passes a SYSTEM user
surfaces the problem at mint time, not at the next request.
The claim shape is built through :class:JwtClaims so the
encode-side payload is statically typed and the decode-side
boundary helper validates against the same model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
User
|
Authenticated human user. |
required |
session_id
|
str | None
|
Reuse this session id ( |
None
|
Returns:
| Type | Description |
|---|---|
tuple[str, int, str]
|
Tuple of (encoded JWT, expiry seconds, session ID). |
Raises:
| Type | Description |
|---|---|
SecretNotConfiguredError
|
If the JWT secret is empty. |
ValueError
|
If user has the SYSTEM role -- mint via the CLI's system-token path instead. |
Source code in src/synthorg/api/auth/service.py
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
decode_token
¶
Decode and validate a JWT into a typed claim set.
Issuer (iss) and audience (aud) verification is
intentionally deferred to the auth middleware's
_resolve_jwt_user: the canonical pair differs by role
(synthorg-cli / synthorg-backend for CLI-minted
SYSTEM tokens vs. synthorg-api / synthorg-api for
API-minted user tokens), and the middleware loads the user
record before deciding which pair to enforce. Both claims are
require-listed here so a missing claim fails decode rather
than reaching the middleware as None.
After PyJWT validates the signature and required claims, the
raw payload is routed through
:func:synthorg.api.boundary.parse_typed so a malformed claim
set (extra keys, type mismatch, iat >= exp) is rejected at
the boundary with a structured api.boundary.validation_failed
log instead of slipping through and surprising a downstream
attribute access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
Encoded JWT string. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Validated |
JwtClaims
|
class: |
Raises:
| Type | Description |
|---|---|
SecretNotConfiguredError
|
If the JWT secret is empty. |
InvalidTokenError
|
If the token signature, expiry, or required claim set is invalid. |
ValidationError
|
If the decoded claim set does not
conform to :class: |
Source code in src/synthorg/api/auth/service.py
persist_refresh_token
async
¶
Persist a refresh token through the auth-domain boundary.
Centralises the refresh-store write + audit log so callers
(notably make_session_cookies) do not reach into
app_state._refresh_store directly. The repo handle is
passed in rather than held by the service so this stays
compatible with the existing AuthService construction (no
constructor change required).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
object
|
The :class: |
required |
token_hash
|
str
|
HMAC-SHA256 hex digest of the raw refresh token. |
required |
session_id
|
str
|
Session identifier. |
required |
user_id
|
str
|
User identifier. |
required |
expires_at
|
datetime
|
Refresh token expiry (UTC). |
required |
Raises:
| Type | Description |
|---|---|
QueryError
|
If the underlying repo write fails. |
Source code in src/synthorg/api/auth/service.py
rotate_refresh_token
async
¶
Single-use refresh rotation: consume, validate, re-mint.
The reject matrix lives here (not the controller) so it is
unit-testable without the full app: a missing / replayed /
expired refresh token or a revoked session emits
SECURITY_AUTH_REFRESH_REJECTED (typed reason) and raises
:class:RefreshTokenInvalidError (HTTP 401, code 1005). The
success path re-mints the access token within the consumed
record's session so rotation does not orphan the session or
saturate max_concurrent_sessions.
SECURITY_AUTH_REFRESH_CONSUMED is emitted by the caller
AFTER the rotated refresh row is persisted (state-transition
events log after the write), so it is intentionally not
emitted here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw_refresh_token
|
str
|
The opaque refresh cookie value. |
required |
refresh_store
|
RefreshTokenRepository
|
Repository providing single-use
|
required |
users
|
UserRepository
|
User repository for the post-consume owner lookup. |
required |
is_session_revoked
|
Callable[[str], bool] | None
|
Predicate passed into |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
RefreshRotation
|
class: |
RefreshRotation
|
the preserved session id. |
Raises:
| Type | Description |
|---|---|
RefreshTokenInvalidError
|
For any reject path (missing cookie, consume rejection, or owner deleted between issuance and rotation). |
Source code in src/synthorg/api/auth/service.py
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 | |
hash_api_key
¶
Compute HMAC-SHA256 hex digest of a raw API key.
Uses the server-side JWT secret as the HMAC key so that an attacker with read access to stored hashes cannot brute-force API keys offline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw_key
|
str
|
The plaintext API key. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Lowercase hex digest. |
Raises:
| Type | Description |
|---|---|
SecretNotConfiguredError
|
If the JWT secret is empty. |
Source code in src/synthorg/api/auth/service.py
generate_api_key
staticmethod
¶
Generate a cryptographically secure API key.
Returns:
| Type | Description |
|---|---|
str
|
URL-safe base64 string sized by |
str
|
(default 32 bytes / 43 base64 chars). |
Source code in src/synthorg/api/auth/service.py
middleware
¶
JWT + API key authentication middleware.
ApiAuthMiddleware
¶
Bases: AbstractAuthenticationMiddleware
Authenticate requests via cookie, JWT header, or API key.
Authentication priority:
- Session cookie: HttpOnly cookie set by login/setup. Primary auth path for browser sessions.
- Authorization header:
Bearer <token>. Tokens with dots are JWTs (system user CLI tokens). Tokens without dots are API keys (HMAC-SHA256 lookup).
Requires auth_service, persistence backend on
app.state["app_state"].
authenticate_request
async
¶
Validate the session cookie or Authorization header.
Tries the session cookie first. Falls back to the Authorization header for API keys and system user JWTs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
ASGIConnection[Any, Any, Any, Any]
|
Incoming ASGI connection. |
required |
Returns:
| Type | Description |
|---|---|
AuthenticationResult
|
AuthenticationResult with AuthenticatedUser. |
Raises:
| Type | Description |
|---|---|
NotAuthorizedException
|
If authentication fails. |
Source code in src/synthorg/api/auth/middleware.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
create_auth_middleware_class
¶
Create a middleware class with excluded paths baked in.
Litestar's AbstractAuthenticationMiddleware.__init__ takes
exclude as a parameter (default None). We create a
subclass whose __init__ forwards the configured exclude
list to super().__init__.
The middleware is restricted to ScopeType.HTTP only;
WebSocket connections use ticket-based auth handled entirely
inside the WS handler (see controllers/ws.py).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
auth_config
|
AuthConfig
|
Auth configuration with exclude_paths. |
required |
Returns:
| Type | Description |
|---|---|
type[ApiAuthMiddleware]
|
Middleware class ready for use in the Litestar middleware stack. |
Source code in src/synthorg/api/auth/middleware.py
context
¶
Request-scoped binding for the authenticated user.
The auth middleware (:class:synthorg.api.auth.middleware.ApiAuthMiddleware)
populates connection.scope["user"] with an
:class:~synthorg.core.auth.models.AuthenticatedUser after authentication.
:class:AuthContextMiddleware runs immediately after auth and binds that
user into the per-:class:asyncio.Task :class:~contextvars.ContextVar
defined here. Controllers and request-coupled helpers then read the
authenticated user via :func:get_authenticated_user_id /
:func:get_authenticated_user without threading a Request argument.
Reading the var while no user is bound raises
:class:AuthContextMissingError (a 500): this surfaces middleware
misconfiguration loudly instead of masking it as "api".
WebSocket scopes use ticket-based authentication
(synthorg.api.controllers.ws) and are not handled by this module;
:class:AuthContextMiddleware is restricted to HTTP scopes.
AuthContextMissingError
¶
Bases: DomainError
Read attempted on the auth ContextVar with no user bound.
Surfacing this as a 500 is intentional: the auth middleware runs
before any controller, so by the time a controller (or helper
invoked from one) calls :func:get_authenticated_user_id the var
must be set. An unset read is therefore a server bug --
exclude_paths misconfiguration, a helper invoked outside the
request lifecycle, or :class:AuthContextMiddleware missing from
the middleware stack -- not a client error.
Source code in src/synthorg/core/domain_errors.py
AuthContextMiddleware
¶
Bases: ASGIMiddleware
Bind scope["user"] into the per-task ContextVar.
Runs immediately after :class:~synthorg.api.auth.middleware.ApiAuthMiddleware
so authenticated handlers, downstream middleware, and helpers can
read the user via :func:get_authenticated_user_id without
threading a Request. Excluded paths (where ApiAuthMiddleware
skipped) leave the var at its default None; helpers reading it
raise :class:AuthContextMissingError, which is the desired
behaviour for endpoints that should never have reached a
user-coupled helper without authentication.
HTTP-only: WebSocket scopes use ticket-based authentication and are
bypassed by the scopes filter on the base class.
handle
async
¶
Bind scope["user"] for the duration of the inner dispatch.
Source code in src/synthorg/api/auth/context.py
get_authenticated_user
¶
Return the user bound to the active request's ContextVar.
Raises:
| Type | Description |
|---|---|
AuthContextMissingError
|
When called outside an authenticated request scope. |
Returns:
| Type | Description |
|---|---|
AuthenticatedUser
|
|
Source code in src/synthorg/api/auth/context.py
get_authenticated_user_id
¶
Return the user_id of the user bound to the current request.
Raises:
| Type | Description |
|---|---|
AuthContextMissingError
|
When called outside an authenticated request scope. |
Returns:
| Type | Description |
|---|---|
str
|
Resulting string. |
Source code in src/synthorg/api/auth/context.py
authenticated_user_scope
async
¶
Bind user to the auth ContextVar for the duration of the block.
Production binding is performed by :class:AuthContextMiddleware.
This helper exists for tests, background tasks, and any caller that
needs to invoke a request-coupled helper outside the HTTP request
path. Mirrors :func:synthorg.providers.cost_recording.cost_recording_scope
-- token-based reset for exception safety, restoring whatever was
active before.
Example (background task that calls a request-coupled helper)::
async def _background_audit(user: AuthenticatedUser) -> None:
async with authenticated_user_scope(user):
# audit_actor_from_context() now returns this user's
# ProviderAuditActor without raising.
actor = audit_actor_from_context()
...