Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion docs/design/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,10 @@ exceptions:
`since < until` ordering.
- `parse_str_sequence(arguments, key)`: optional sequence-of-non-blank-strings.
- `coerce_pagination(arguments, *, default_limit=50)`: offset/limit
parsing with strict bounds and explicit bool rejection.
parsing with strict bounds and explicit bool rejection. MCP tools
default to 50; this is intentionally lower than the repository-layer
`DEFAULT_LIST_LIMIT = 100` so paginated MCP responses stay terse for
assistants.
- `actor_id(actor)` / `require_actor_id(actor)` / `actor_label(actor)`:
actor identity helpers. Use `actor_id` for optional attribution,
`require_actor_id` when attribution is mandatory (raises if
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/mcp-handler-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Use the helpers in `common_args.py` for tools without `args_model`:
- `require_dict(arguments, key, *, value_type=None, deep_copy=True)` for dict args; pass `value_type=str` for `dict[str, str]` validation.
- `parse_time_window(arguments, *, until_required=True)` for ISO 8601 since/until parsing.
- `parse_str_sequence(arguments, key)` for optional sequence-of-non-blank-strings args.
- `coerce_pagination(arguments, *, default_limit=50)` for offset/limit with bool rejection and bound enforcement.
- `coerce_pagination(arguments, *, default_limit=50)` for offset/limit with bool rejection and bound enforcement. (MCP tools default to 50; this is intentionally lower than the repository-layer `DEFAULT_LIST_LIMIT = 100` so paginated MCP responses stay terse for assistants.)

For actor identity: use `actor_id(actor)` for optional attribution, `require_actor_id(actor)` when attribution is mandatory (raises if missing), and `actor_label(actor)` only for emit-only paths where a `"mcp-anonymous"` fallback is acceptable.

Expand Down
37 changes: 0 additions & 37 deletions scripts/list_pagination_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,3 @@
#
# Regenerate (rare; requires explicit user approval) with:
# uv run python scripts/check_list_pagination.py --update
src/synthorg/persistence/custom_rule_protocol.py:CustomRuleRepository.list_rules:missing-limit-param
src/synthorg/persistence/postgres/custom_rule_repo.py:PostgresCustomRuleRepository.list_rules:missing-limit-param
src/synthorg/persistence/postgres/hr_repositories.py:PostgresCollaborationMetricRepository.query:missing-limit-param
src/synthorg/persistence/postgres/hr_repositories.py:PostgresTaskMetricRepository.query:missing-limit-param
src/synthorg/persistence/postgres/preset_repo.py:PostgresPersonalityPresetRepository.list_all:missing-limit-param
src/synthorg/persistence/postgres/project_repo.py:PostgresProjectRepository.list_projects:missing-limit-param
src/synthorg/persistence/postgres/risk_override_repo.py:PostgresRiskOverrideRepository.list_active:missing-limit-param
src/synthorg/persistence/postgres/subworkflow_repo.py:PostgresSubworkflowRepository.list_summaries:missing-limit-param
src/synthorg/persistence/postgres/subworkflow_repo.py:PostgresSubworkflowRepository.list_versions:missing-limit-param
src/synthorg/persistence/postgres/training_plan_repo.py:PostgresTrainingPlanRepository.list_by_agent:missing-limit-param
src/synthorg/persistence/postgres/user_repo.py:PostgresUserRepository.list_users:missing-limit-param
src/synthorg/persistence/postgres/workflow_definition_repo.py:PostgresWorkflowDefinitionRepository.list_definitions:missing-limit-param
src/synthorg/persistence/postgres/workflow_execution_repo.py:PostgresWorkflowExecutionRepository.list_by_definition:missing-limit-param
src/synthorg/persistence/postgres/workflow_execution_repo.py:PostgresWorkflowExecutionRepository.list_by_status:missing-limit-param
src/synthorg/persistence/preset_protocol.py:PersonalityPresetRepository.list_all:missing-limit-param
src/synthorg/persistence/project_protocol.py:ProjectRepository.list_projects:missing-limit-param
src/synthorg/persistence/risk_override_protocol.py:RiskOverrideRepository.list_active:missing-limit-param
src/synthorg/persistence/sqlite/custom_rule_repo.py:SQLiteCustomRuleRepository.list_rules:missing-limit-param
src/synthorg/persistence/sqlite/hr_repositories.py:SQLiteCollaborationMetricRepository.query:missing-limit-param
src/synthorg/persistence/sqlite/hr_repositories.py:SQLiteTaskMetricRepository.query:missing-limit-param
src/synthorg/persistence/sqlite/preset_repo.py:SQLitePersonalityPresetRepository.list_all:missing-limit-param
src/synthorg/persistence/sqlite/project_repo.py:SQLiteProjectRepository.list_projects:missing-limit-param
src/synthorg/persistence/sqlite/risk_override_repo.py:SQLiteRiskOverrideRepository.list_active:missing-limit-param
src/synthorg/persistence/sqlite/subworkflow_repo.py:SQLiteSubworkflowRepository.list_summaries:missing-limit-param
src/synthorg/persistence/sqlite/subworkflow_repo.py:SQLiteSubworkflowRepository.list_versions:missing-limit-param
src/synthorg/persistence/sqlite/training_plan_repo.py:SQLiteTrainingPlanRepository.list_by_agent:missing-limit-param
src/synthorg/persistence/sqlite/user_repo.py:SQLiteUserRepository.list_users:missing-limit-param
src/synthorg/persistence/sqlite/workflow_definition_repo.py:SQLiteWorkflowDefinitionRepository.list_definitions:missing-limit-param
src/synthorg/persistence/sqlite/workflow_execution_repo.py:SQLiteWorkflowExecutionRepository.list_by_definition:missing-limit-param
src/synthorg/persistence/sqlite/workflow_execution_repo.py:SQLiteWorkflowExecutionRepository.list_by_status:missing-limit-param
src/synthorg/persistence/subworkflow_protocol.py:SubworkflowRepository.list_summaries:missing-limit-param
src/synthorg/persistence/subworkflow_protocol.py:SubworkflowRepository.list_versions:missing-limit-param
src/synthorg/persistence/training_protocol.py:TrainingPlanRepository.list_by_agent:missing-limit-param
src/synthorg/persistence/user_protocol.py:UserRepository.list_users:missing-limit-param
src/synthorg/persistence/workflow_definition_protocol.py:WorkflowDefinitionRepository.list_definitions:missing-limit-param
src/synthorg/persistence/workflow_execution_protocol.py:WorkflowExecutionRepository.list_by_definition:missing-limit-param
src/synthorg/persistence/workflow_execution_protocol.py:WorkflowExecutionRepository.list_by_status:missing-limit-param
26 changes: 0 additions & 26 deletions scripts/long_running_loops_kill_switch_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,3 @@
# New violations not on this list fail the gate; entries here
# that no longer match the scan emit a stale-baseline warning
# (gate still passes; regenerate via --update-baseline).
src/synthorg/api/controllers/ws.py:_outbound_consumer:341
src/synthorg/api/controllers/ws.py:_receive_loop:735
src/synthorg/api/controllers/ws_revalidation.py:_periodic_revalidate:112
src/synthorg/api/services/idempotency_service.py:IdempotencyService:run_idempotent:185
src/synthorg/backup/scheduler.py:BackupScheduler:_run_loop:279
src/synthorg/budget/quota_poller.py:QuotaPoller:_poll_loop:137
src/synthorg/communication/bus/_nats_history.py:collect_history_batches:89
src/synthorg/communication/bus/_nats_receive.py:receive_blocking:388
src/synthorg/communication/bus/_nats_receive.py:receive_with_timeout:428
src/synthorg/communication/conflict_resolution/escalation/in_memory_store.py:InMemoryEscalationStore:_never:304
src/synthorg/communication/event_stream/stream.py:EventStreamHub:_janitor_loop:348
src/synthorg/communication/meeting/scheduler.py:MeetingScheduler:_run_periodic:492
src/synthorg/engine/task_engine_loops.py:TaskEngineLoopsMixin:_observer_dispatch_loop:303
src/synthorg/hr/pruning/service.py:PruningService:_run_loop:769
src/synthorg/integrations/health/prober.py:HealthProberService:_probe_loop:156
src/synthorg/integrations/oauth/token_manager.py:OAuthTokenManager:_refresh_loop:144
src/synthorg/integrations/rate_limiting/shared_state.py:SharedRateLimitCoordinator:_poll_loop:225
src/synthorg/meta/chief_of_staff/monitor.py:OrgInflectionMonitor:_loop:192
src/synthorg/persistence/sqlite/escalation_repo.py:SQLiteEscalationRepository:_never:331
src/synthorg/providers/resilience/rate_limiter.py:RateLimiter:_wait_for_rpm_slot:148
src/synthorg/providers/resilience/rate_limiter.py:RateLimiter:acquire:71
src/synthorg/security/timeout/scheduler.py:ApprovalTimeoutScheduler:_run_loop:288
src/synthorg/settings/dispatcher.py:SettingsChangeDispatcher:_poll_loop:414
src/synthorg/telemetry/collector.py:TelemetryCollector:_heartbeat_loop:777
src/synthorg/tools/sandbox/lifecycle/per_agent.py:PerAgentStrategy:_idle_expire:277
src/synthorg/workers/worker.py:Worker:run:115
31 changes: 0 additions & 31 deletions scripts/loop_bound_init_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,3 @@
# `path:line:class:attr:primitive`. When fixing one of these
# sites, regenerate this file via:
# uv run python scripts/check_no_loop_bound_init.py --update-baseline

src/synthorg/api/bus_bridge.py:102:MessageBusBridge:_lifecycle_lock:Lock
src/synthorg/backup/service.py:75:BackupService:_backup_lock:Lock
src/synthorg/budget/quota_poller.py:64:QuotaPoller:_lifecycle_lock:Lock
src/synthorg/communication/bus/memory.py:110:InMemoryMessageBus:_lock:Lock
src/synthorg/communication/bus/memory.py:125:InMemoryMessageBus:_shutdown_event:Event
src/synthorg/engine/task_engine.py:104:TaskEngine:_lifecycle_lock:Lock
src/synthorg/engine/task_engine.py:112:TaskEngine:_admission_lock:Lock
src/synthorg/engine/task_engine.py:114:TaskEngine:_observer_queue:Queue
src/synthorg/engine/task_engine.py:96:TaskEngine:_queue:Queue
src/synthorg/hr/pruning/service.py:103:PruningService:_lifecycle_lock:Lock
src/synthorg/hr/pruning/service.py:129:PruningService:_processing_lock:Lock
src/synthorg/hr/pruning/service.py:97:PruningService:_wake_event:Event
src/synthorg/hr/pruning/service.py:98:PruningService:_stop_event:Event
src/synthorg/integrations/health/prober.py:131:HealthProberService:_failure_lock:Lock
src/synthorg/integrations/health/prober.py:133:HealthProberService:_lifecycle_lock:Lock
src/synthorg/integrations/oauth/token_manager.py:70:OAuthTokenManager:_lifecycle_lock:Lock
src/synthorg/integrations/rate_limiting/shared_state.py:71:SharedRateLimitCoordinator:_window_lock:Lock
src/synthorg/integrations/rate_limiting/shared_state.py:80:SharedRateLimitCoordinator:_lifecycle_lock:Lock
src/synthorg/integrations/tunnel/ngrok_adapter.py:76:NgrokAdapter:_lifecycle_lock:Lock
src/synthorg/memory/embedding/fine_tune_orchestrator.py:99:FineTuneOrchestrator:_op_lock:Lock
src/synthorg/meta/chief_of_staff/monitor.py:82:OrgInflectionMonitor:_stop_event:Event
src/synthorg/meta/chief_of_staff/monitor.py:83:OrgInflectionMonitor:_lifecycle_lock:Lock
src/synthorg/notifications/adapters/ntfy.py:116:NtfyNotificationSink:_lifecycle_lock:Lock
src/synthorg/notifications/adapters/slack.py:104:SlackNotificationSink:_lifecycle_lock:Lock
src/synthorg/notifications/dispatcher.py:113:NotificationDispatcher:_dispatch_idle:Event
src/synthorg/notifications/dispatcher.py:92:NotificationDispatcher:_lifecycle_lock:Lock
src/synthorg/settings/dispatcher.py:105:SettingsChangeDispatcher:_lifecycle_lock:Lock
src/synthorg/telemetry/collector.py:321:TelemetryCollector:_lifecycle_lock:Lock
src/synthorg/workers/worker.py:83:Worker:_stop_event:Event
src/synthorg/workers/worker.py:92:Worker:_lifecycle_lock:Lock
20 changes: 10 additions & 10 deletions scripts/no_magic_numbers_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ src/synthorg/client/generators/llm.py:57:26
src/synthorg/client/generators/procedural.py:66:38
src/synthorg/communication/bus/_nats_history.py:74:22
src/synthorg/communication/bus/_nats_history.py:75:35
src/synthorg/communication/bus/_nats_history.py:156:22
src/synthorg/communication/bus/_nats_history.py:157:35
src/synthorg/communication/bus/_nats_history.py:202:22
src/synthorg/communication/bus/_nats_history.py:203:35
src/synthorg/communication/bus/_nats_history.py:157:22
src/synthorg/communication/bus/_nats_history.py:158:35
src/synthorg/communication/bus/_nats_history.py:203:22
src/synthorg/communication/bus/_nats_history.py:204:35
src/synthorg/communication/conflict_resolution/escalation/config.py:15:26
src/synthorg/communication/conflict_resolution/escalation/in_memory_store.py:34:17
src/synthorg/communication/conflict_resolution/escalation/protocol.py:21:17
Expand Down Expand Up @@ -194,7 +194,7 @@ src/synthorg/hr/performance/quality_override_store.py:26:25
src/synthorg/hr/performance/theil_sen_strategy.py:61:31
src/synthorg/hr/performance/theil_sen_strategy.py:62:37
src/synthorg/hr/performance/theil_sen_strategy.py:63:37
src/synthorg/hr/persistence_protocol.py:42:21
src/synthorg/hr/persistence_protocol.py:43:21
src/synthorg/hr/scaling/guards/approval_gate.py:49:27
src/synthorg/hr/scaling/guards/conflict_resolver.py:30:19
src/synthorg/hr/scaling/guards/cooldown.py:34:32
Expand Down Expand Up @@ -437,7 +437,7 @@ src/synthorg/persistence/postgres/decision_repo.py:514:21
src/synthorg/persistence/postgres/escalation_repo.py:42:17
src/synthorg/persistence/postgres/fine_tune_repo.py:244:21
src/synthorg/persistence/postgres/fine_tune_repo.py:456:21
src/synthorg/persistence/postgres/hr_repositories.py:116:21
src/synthorg/persistence/postgres/hr_repositories.py:120:21
src/synthorg/persistence/postgres/mcp_installation_repo.py:134:21
src/synthorg/persistence/postgres/ontology_drift_repo.py:105:21
src/synthorg/persistence/postgres/ontology_drift_repo.py:132:21
Expand All @@ -453,7 +453,7 @@ src/synthorg/persistence/postgres/session_repo.py:127:21
src/synthorg/persistence/postgres/session_repo.py:155:21
src/synthorg/persistence/postgres/settings_repo.py:127:21
src/synthorg/persistence/postgres/ssrf_violation_repo.py:143:21
src/synthorg/persistence/postgres/user_repo.py:513:21
src/synthorg/persistence/postgres/user_repo.py:535:21
src/synthorg/persistence/postgres/version_repo.py:339:21
src/synthorg/persistence/postgres/webhook_receipt_repo.py:147:21
src/synthorg/persistence/provider_audit_protocol.py:62:21
Expand All @@ -467,7 +467,7 @@ src/synthorg/persistence/sqlite/decision_repo.py:556:21
src/synthorg/persistence/sqlite/escalation_repo.py:36:17
src/synthorg/persistence/sqlite/fine_tune_repo.py:212:21
src/synthorg/persistence/sqlite/fine_tune_repo.py:419:21
src/synthorg/persistence/sqlite/hr_repositories.py:113:21
src/synthorg/persistence/sqlite/hr_repositories.py:117:21
src/synthorg/persistence/sqlite/mcp_installation_repo.py:109:21
src/synthorg/persistence/sqlite/ontology_drift_repo.py:112:21
src/synthorg/persistence/sqlite/ontology_drift_repo.py:129:21
Expand All @@ -483,12 +483,12 @@ src/synthorg/persistence/sqlite/session_repo.py:145:21
src/synthorg/persistence/sqlite/session_repo.py:167:21
src/synthorg/persistence/sqlite/settings_repo.py:98:21
src/synthorg/persistence/sqlite/ssrf_violation_repo.py:156:21
src/synthorg/persistence/sqlite/user_repo.py:679:21
src/synthorg/persistence/sqlite/user_repo.py:695:21
src/synthorg/persistence/sqlite/version_repo.py:331:21
src/synthorg/persistence/sqlite/webhook_receipt_repo.py:126:21
src/synthorg/persistence/ssrf_violation_protocol.py:49:21
src/synthorg/persistence/task_protocol.py:45:21
src/synthorg/persistence/user_protocol.py:189:21
src/synthorg/persistence/user_protocol.py:202:21
src/synthorg/persistence/version_protocol.py:107:21
src/synthorg/providers/drivers/litellm_driver.py:97:24
src/synthorg/providers/health.py:37:23
Expand Down
17 changes: 14 additions & 3 deletions src/synthorg/api/auth/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SECURITY_USER_DELETED,
SECURITY_USER_UPDATED,
)
from synthorg.persistence._shared import DEFAULT_LIST_LIMIT

if TYPE_CHECKING:
from synthorg.persistence.auth_protocol import RefreshTokenRepository
Expand Down Expand Up @@ -69,9 +70,19 @@ async def get(self, user_id: NotBlankStr) -> User | None:
"""Fetch a user by id, or ``None`` when no row matches."""
return await self._repo.get(user_id)

async def list_users(self) -> tuple[User, ...]:
"""List all users (sans system user)."""
users = await self._repo.list_users()
async def list_users(
self,
*,
limit: int = DEFAULT_LIST_LIMIT,
) -> tuple[User, ...]:
"""List human users (sans system user), capped at ``limit`` rows.

Bounded by *limit* (default :data:`DEFAULT_LIST_LIMIT`) so an
unauth'd caller cannot materialise an unbounded tuple of users.
Callers paginating over large user bases should use
:meth:`list_users_page` instead, which exposes a stable cursor.
"""
users = await self._repo.list_users(limit=limit)
logger.debug(API_USER_LISTED, count=len(users))
return users

Expand Down
4 changes: 3 additions & 1 deletion src/synthorg/api/bus_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ def __init__(
# _running is atomic against concurrent lifecycle calls.
# Does not gate publish / receive (those use the underlying
# bus lock) so normal traffic is not serialized here.
self._lifecycle_lock = asyncio.Lock()
# Eager init: stop() must be safe to call before start() has
# ever run, so a half-published lock attribute would race.
self._lifecycle_lock = asyncio.Lock() # lint-allow: loop-bound-init -- see.
# Resolver-failure warnings are logged only on the first
# failure in a run of failures to avoid flooding logs during
# a prolonged settings outage. The flag is cleared on the
Expand Down
4 changes: 4 additions & 0 deletions src/synthorg/api/controllers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,13 @@ async def list_projects(
)
raise ValidationError(msg) from exc

# Over-fetch by one page so the cursor paginator can detect
# has_more without a separate COUNT round-trip. ``limit + 1``
# caps repository-side scans at the operator-tunable page size.
projects = await _service(state).list_projects(
status=parsed_status,
lead=lead,
limit=limit + 1,
)
page, meta = paginate_cursor(
projects,
Expand Down
4 changes: 3 additions & 1 deletion src/synthorg/api/controllers/workflow_executions.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ async def list_executions(
) -> Response[PaginatedResponse[WorkflowExecution] | ApiResponse[None]]:
"""List executions for a workflow definition with cursor pagination."""
service = await _build_service(state)
executions = await service.list_executions(workflow_id)
# Over-fetch by one page so the cursor paginator can detect
# has_more without a separate COUNT round-trip.
executions = await service.list_executions(workflow_id, limit=limit + 1)
page, meta = paginate_cursor(
tuple(executions),
limit=limit,
Expand Down
7 changes: 6 additions & 1 deletion src/synthorg/api/controllers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ async def list_workflows(
msg = f"Invalid workflow type: {workflow_type!r}. Valid: {valid}"
raise WorkflowTypeInvalidError(msg) from exc

defs = await _service(state).list_definitions(workflow_type=parsed_type)
# Over-fetch by one page so the cursor paginator can detect
# has_more without a separate COUNT round-trip.
defs = await _service(state).list_definitions(
workflow_type=parsed_type,
limit=limit + 1,
)
page, meta = paginate_cursor(
defs,
limit=limit,
Expand Down
2 changes: 2 additions & 0 deletions src/synthorg/api/controllers/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ async def _outbound_consumer(
the socket with code 1011 and exits; the surrounding
``run_in_background`` context tears the subscription down.
"""
# lint-allow: long-running-loop-kill-switch -- per-request WS consumer.
while True:
event_data = await queue.get()
try:
Expand Down Expand Up @@ -732,6 +733,7 @@ async def _receive_loop( # noqa: PLR0913 -- one extra optional kw arg for the t
app_state = socket.app.state["app_state"]
frame_timeout_seconds = app_state.ws_frame_timeout_seconds
try:
# lint-allow: long-running-loop-kill-switch -- per-request WS receive.
while True:
try:
data = await asyncio.wait_for(
Expand Down
1 change: 1 addition & 0 deletions src/synthorg/api/controllers/ws_revalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ async def _periodic_revalidate(
max_events=max_failures,
window_seconds=float(window),
)
# lint-allow: long-running-loop-kill-switch -- per-connection revalidate.
while True:
try:
await asyncio.sleep(interval_seconds)
Expand Down
1 change: 1 addition & 0 deletions src/synthorg/api/services/idempotency_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ async def run_idempotent(
churn under sustained leader failures.
"""
retries_after_leader_failure = 0
# lint-allow: long-running-loop-kill-switch -- per-request retry-wait.
while True:
outcome_value, fresh, timed_out = await self._run_idempotent_once(
scope=scope,
Expand Down
Loading
Loading