Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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