marvisx-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/api/__init__.py +0 -0
- core/api/agents/__init__.py +0 -0
- core/api/agents/session_health.py +59 -0
- core/api/agents/session_manager.py +206 -0
- core/api/bin/marvisx-state-hook.py +182 -0
- core/api/config.py +533 -0
- core/api/db.py +1516 -0
- core/api/dependencies/__init__.py +0 -0
- core/api/dependencies/tenant.py +34 -0
- core/api/main.py +1641 -0
- core/api/mcp/__init__.py +8 -0
- core/api/mcp/_adapter.py +184 -0
- core/api/mcp/server.py +58 -0
- core/api/mcp/tools/__init__.py +59 -0
- core/api/mcp/tools/brain.py +599 -0
- core/api/mcp/tools/graph.py +380 -0
- core/api/mcp/tools/handoffs.py +112 -0
- core/api/mcp/tools/ingest.py +326 -0
- core/api/mcp/tools/learnings.py +144 -0
- core/api/mcp/tools/projects.py +99 -0
- core/api/mcp/tools/pull_requests.py +173 -0
- core/api/mcp/tools/safety.py +111 -0
- core/api/mcp/tools/search.py +79 -0
- core/api/mcp/tools/tasks.py +258 -0
- core/api/middleware/__init__.py +0 -0
- core/api/middleware/tool_call_audit.py +111 -0
- core/api/models/__init__.py +346 -0
- core/api/models/auth.py +48 -0
- core/api/models/brain.py +1006 -0
- core/api/models/common.py +76 -0
- core/api/models/costs.py +91 -0
- core/api/models/graph.py +66 -0
- core/api/models/graph_cosmo.py +125 -0
- core/api/models/graph_pr_impact.py +257 -0
- core/api/models/graph_ux.py +141 -0
- core/api/models/inbox.py +230 -0
- core/api/models/ingest_keys.py +108 -0
- core/api/models/kg.py +41 -0
- core/api/models/llm_config.py +56 -0
- core/api/models/monitoring.py +234 -0
- core/api/models/projects.py +161 -0
- core/api/models/search.py +42 -0
- core/api/models/sessions.py +322 -0
- core/api/models/tasks.py +184 -0
- core/api/models/teams.py +63 -0
- core/api/models/users.py +184 -0
- core/api/observability/__init__.py +0 -0
- core/api/observability/tracing.py +92 -0
- core/api/paths.py +26 -0
- core/api/rate_limit.py +24 -0
- core/api/rbac.py +112 -0
- core/api/routers/__init__.py +0 -0
- core/api/routers/_adapter.py +24 -0
- core/api/routers/admin_pr_impact.py +230 -0
- core/api/routers/admin_settings.py +147 -0
- core/api/routers/agent.py +1079 -0
- core/api/routers/agent_tokens.py +276 -0
- core/api/routers/app_settings.py +112 -0
- core/api/routers/audit.py +89 -0
- core/api/routers/auth.py +586 -0
- core/api/routers/bench.py +161 -0
- core/api/routers/brain.py +881 -0
- core/api/routers/brain_directions.py +527 -0
- core/api/routers/ci_checks.py +140 -0
- core/api/routers/comments.py +273 -0
- core/api/routers/costs.py +148 -0
- core/api/routers/docs_coverage.py +217 -0
- core/api/routers/docs_governance.py +63 -0
- core/api/routers/documents.py +318 -0
- core/api/routers/files.py +163 -0
- core/api/routers/finder.py +987 -0
- core/api/routers/graph.py +836 -0
- core/api/routers/handoffs.py +156 -0
- core/api/routers/inbox.py +496 -0
- core/api/routers/ingest_api_keys.py +205 -0
- core/api/routers/ingest_triage.py +1227 -0
- core/api/routers/judge.py +306 -0
- core/api/routers/kg.py +336 -0
- core/api/routers/learnings.py +253 -0
- core/api/routers/llm_config.py +130 -0
- core/api/routers/monitoring.py +347 -0
- core/api/routers/notifications.py +125 -0
- core/api/routers/pr_impact.py +315 -0
- core/api/routers/projects.py +1061 -0
- core/api/routers/pull_requests.py +312 -0
- core/api/routers/push.py +67 -0
- core/api/routers/raci.py +228 -0
- core/api/routers/search.py +125 -0
- core/api/routers/sessions.py +3100 -0
- core/api/routers/settings.py +90 -0
- core/api/routers/share_repo.py +68 -0
- core/api/routers/status_updates.py +96 -0
- core/api/routers/tags.py +45 -0
- core/api/routers/tasks.py +526 -0
- core/api/routers/teams.py +425 -0
- core/api/routers/terminal.py +105 -0
- core/api/routers/users.py +331 -0
- core/api/routers/webhooks.py +330 -0
- core/api/runtime_settings.py +84 -0
- core/api/security.py +652 -0
- core/api/services/__init__.py +0 -0
- core/api/services/audit.py +58 -0
- core/api/services/auto_approval.py +82 -0
- core/api/services/brain/__init__.py +52 -0
- core/api/services/brain/baseline.py +230 -0
- core/api/services/brain/capabilities.py +75 -0
- core/api/services/brain/cascade_rollup.py +388 -0
- core/api/services/brain/compound_bridge.py +215 -0
- core/api/services/brain/cycle.py +1242 -0
- core/api/services/brain/cycle_snapshot.py +371 -0
- core/api/services/brain/digest_collector.py +147 -0
- core/api/services/brain/direction.py +421 -0
- core/api/services/brain/drift.py +356 -0
- core/api/services/brain/drift_router.py +409 -0
- core/api/services/brain/edge_metrics.py +79 -0
- core/api/services/brain/events_reader.py +222 -0
- core/api/services/brain/findings.py +1379 -0
- core/api/services/brain/findings_reader.py +1006 -0
- core/api/services/brain/jobs.py +733 -0
- core/api/services/brain/journal.py +206 -0
- core/api/services/brain/knowledge_forms.py +92 -0
- core/api/services/brain/llm/__init__.py +37 -0
- core/api/services/brain/llm/_runner.py +62 -0
- core/api/services/brain/llm/base.py +70 -0
- core/api/services/brain/llm/cache.py +99 -0
- core/api/services/brain/llm/constants.py +46 -0
- core/api/services/brain/llm/direction_alignment.py +289 -0
- core/api/services/brain/llm/factory.py +132 -0
- core/api/services/brain/llm/finding_reasoning.py +98 -0
- core/api/services/brain/llm/finding_summary.py +92 -0
- core/api/services/brain/llm/grounding.py +46 -0
- core/api/services/brain/llm/journal_polish.py +96 -0
- core/api/services/brain/llm/local_gateway.py +426 -0
- core/api/services/brain/llm/parsers.py +71 -0
- core/api/services/brain/llm/router_glue.py +422 -0
- core/api/services/brain/memory_ops.py +1677 -0
- core/api/services/brain/models.py +140 -0
- core/api/services/brain/owner_hint.py +211 -0
- core/api/services/brain/recap.py +307 -0
- core/api/services/brain/rules/__init__.py +65 -0
- core/api/services/brain/rules/_signals.py +205 -0
- core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
- core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
- core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
- core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
- core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
- core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
- core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
- core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
- core/api/services/brain/runs_reader.py +485 -0
- core/api/services/brain/scope.py +79 -0
- core/api/services/brain/sources/__init__.py +46 -0
- core/api/services/brain/sources/base.py +86 -0
- core/api/services/brain/sources/git_kg.py +393 -0
- core/api/services/brain/sources/handoffs.py +157 -0
- core/api/services/brain/sources/ingestor.py +130 -0
- core/api/services/brain/sources/learnings.py +121 -0
- core/api/services/brain/sources/pir_tasks.py +245 -0
- core/api/services/brain/watermarks.py +147 -0
- core/api/services/brain/ws_emitter.py +170 -0
- core/api/services/cc_tasks_reader.py +76 -0
- core/api/services/ci_service.py +263 -0
- core/api/services/claude_metrics.py +796 -0
- core/api/services/codex_metrics.py +364 -0
- core/api/services/conversation_reader.py +102 -0
- core/api/services/cost_service.py +243 -0
- core/api/services/crypto.py +147 -0
- core/api/services/docs_governance/__init__.py +1 -0
- core/api/services/docs_governance/confidence.py +230 -0
- core/api/services/docs_governance/config.py +87 -0
- core/api/services/docs_governance/enrichment.py +65 -0
- core/api/services/docs_governance/frontmatter_validator.py +83 -0
- core/api/services/docs_governance/hard_gates.py +221 -0
- core/api/services/docs_governance/triage_orchestrator.py +98 -0
- core/api/services/embedding_internal.py +395 -0
- core/api/services/embedding_service.py +832 -0
- core/api/services/event_dispatcher.py +167 -0
- core/api/services/events.py +70 -0
- core/api/services/git_ops.py +621 -0
- core/api/services/graph_cosmo_service.py +440 -0
- core/api/services/graph_ranker.py +306 -0
- core/api/services/graph_service.py +1589 -0
- core/api/services/inbox.py +800 -0
- core/api/services/inbox_digest.py +221 -0
- core/api/services/inbox_digest_deep_research.py +80 -0
- core/api/services/inbox_digest_jobs.py +595 -0
- core/api/services/inbox_gmail_sync.py +167 -0
- core/api/services/inbox_llm_classifier.py +906 -0
- core/api/services/inbox_source_identity.py +116 -0
- core/api/services/inbox_sources.py +456 -0
- core/api/services/inbox_taxonomy.py +195 -0
- core/api/services/inbox_tldr.py +1079 -0
- core/api/services/inbox_triage.py +899 -0
- core/api/services/ingest/__init__.py +13 -0
- core/api/services/ingest/api_key_auth.py +136 -0
- core/api/services/ingest/auto_approve.py +120 -0
- core/api/services/ingest/classifier.py +138 -0
- core/api/services/ingest/confidence.py +173 -0
- core/api/services/ingest/dispatch.py +88 -0
- core/api/services/ingest/embedding_router.py +272 -0
- core/api/services/ingest/events.py +33 -0
- core/api/services/ingest/ignore_patterns.py +79 -0
- core/api/services/ingest/image_probe.py +218 -0
- core/api/services/ingest/ingress.py +263 -0
- core/api/services/ingest/insert_saga.py +793 -0
- core/api/services/ingest/llm/__init__.py +13 -0
- core/api/services/ingest/llm/anthropic_haiku.py +23 -0
- core/api/services/ingest/llm/base.py +59 -0
- core/api/services/ingest/llm/byok_provider.py +130 -0
- core/api/services/ingest/llm/classification_context.py +301 -0
- core/api/services/ingest/llm/config_store.py +246 -0
- core/api/services/ingest/llm/factory.py +24 -0
- core/api/services/ingest/llm/kg_enricher.py +306 -0
- core/api/services/ingest/llm/local_gateway.py +821 -0
- core/api/services/ingest/llm/local_vllm.py +23 -0
- core/api/services/ingest/llm/openai_nano.py +349 -0
- core/api/services/ingest/lock_advisory.py +57 -0
- core/api/services/ingest/parser_router.py +1756 -0
- core/api/services/ingest/parsers/__init__.py +1 -0
- core/api/services/ingest/parsers/docling_parser.py +142 -0
- core/api/services/ingest/parsers/docparse_gateway.py +178 -0
- core/api/services/ingest/parsers/docx_parser.py +127 -0
- core/api/services/ingest/parsers/folder_unpacker.py +85 -0
- core/api/services/ingest/parsers/gateway_aux.py +147 -0
- core/api/services/ingest/parsers/image_parser.py +251 -0
- core/api/services/ingest/parsers/internal_markdown.py +89 -0
- core/api/services/ingest/parsers/ocr_gateway.py +117 -0
- core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
- core/api/services/ingest/parsers/pdf_types.py +13 -0
- core/api/services/ingest/parsers/transcript_parser.py +445 -0
- core/api/services/ingest/parsers/vision_gateway.py +186 -0
- core/api/services/ingest/parsers/xlsx_parser.py +91 -0
- core/api/services/ingest/parsers/zip_unpacker.py +126 -0
- core/api/services/ingest/preflight.py +393 -0
- core/api/services/ingest/retry_voyage.py +88 -0
- core/api/services/ingest/routing_policy.py +307 -0
- core/api/services/ingest/serializers/__init__.py +1 -0
- core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
- core/api/services/ingest/skip_log.py +74 -0
- core/api/services/ingest/watcher.py +637 -0
- core/api/services/kg/__init__.py +0 -0
- core/api/services/kg/audit.py +49 -0
- core/api/services/kg/hybrid_search.py +691 -0
- core/api/services/kg/lens.py +339 -0
- core/api/services/kg/pr_impact.py +770 -0
- core/api/services/kg/queries.py +152 -0
- core/api/services/kg/ranking.py +89 -0
- core/api/services/kg/rrf.py +143 -0
- core/api/services/kg_watcher_control.py +161 -0
- core/api/services/local_llm/__init__.py +19 -0
- core/api/services/local_llm/async_client.py +385 -0
- core/api/services/local_llm/client.py +173 -0
- core/api/services/local_llm/url_validator.py +44 -0
- core/api/services/metrics_collector.py +646 -0
- core/api/services/metrics_providers.py +65 -0
- core/api/services/model_registry.py +266 -0
- core/api/services/model_router.py +137 -0
- core/api/services/n8n_client.py +77 -0
- core/api/services/newsletter_llm_gateway.py +66 -0
- core/api/services/notification_service.py +134 -0
- core/api/services/openai_responses.py +55 -0
- core/api/services/opencode_metrics.py +375 -0
- core/api/services/opencode_sessions.py +173 -0
- core/api/services/pii_redactor.py +138 -0
- core/api/services/pr_impact_pipeline/__init__.py +21 -0
- core/api/services/pr_impact_pipeline/differ.py +421 -0
- core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
- core/api/services/pr_impact_pipeline/gc.py +93 -0
- core/api/services/pr_impact_pipeline/languages.py +192 -0
- core/api/services/pr_impact_pipeline/parser.py +178 -0
- core/api/services/pr_impact_pipeline/writer.py +394 -0
- core/api/services/pr_service.py +1393 -0
- core/api/services/project_paths.py +70 -0
- core/api/services/project_status_updates.py +265 -0
- core/api/services/providers.py +276 -0
- core/api/services/push_service.py +170 -0
- core/api/services/reminder_service.py +89 -0
- core/api/services/runas.py +41 -0
- core/api/services/salience_service.py +69 -0
- core/api/services/security_collector.py +281 -0
- core/api/services/session_catalog.py +385 -0
- core/api/services/session_metrics_service.py +301 -0
- core/api/services/session_ops.py +272 -0
- core/api/services/session_state.py +173 -0
- core/api/services/share_links.py +222 -0
- core/api/services/task_transitions.py +146 -0
- core/api/services/terminal_metrics.py +462 -0
- core/api/services/terminal_metrics_dump.py +203 -0
- core/api/services/tmux.py +1205 -0
- core/api/services/webhook_service.py +422 -0
- core/api/services/workspace_sync.py +164 -0
- core/api/templates/__init__.py +1 -0
- core/api/templates/markdown_share.py +164 -0
- core/api/terminal.py +1031 -0
- core/api/tests/__init__.py +0 -0
- core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
- core/api/tests/test_audit_permissions.py +133 -0
- core/api/tests/test_backfill_session_conversations.py +90 -0
- core/api/tests/test_backfill_working_seconds_msg.py +129 -0
- core/api/tests/test_claude_metrics.py +326 -0
- core/api/tests/test_codex_metrics.py +189 -0
- core/api/tests/test_finder_paths.py +74 -0
- core/api/tests/test_git_ops_merge.py +155 -0
- core/api/tests/test_learnings_check_search.py +81 -0
- core/api/tests/test_metrics_providers.py +133 -0
- core/api/tests/test_migration_087.py +164 -0
- core/api/tests/test_migration_088.py +94 -0
- core/api/tests/test_migration_089.py +116 -0
- core/api/tests/test_openai_responses.py +24 -0
- core/api/tests/test_opencode_metrics.py +740 -0
- core/api/tests/test_opencode_sessions.py +321 -0
- core/api/tests/test_pr_workflow_e2e.py +457 -0
- core/api/tests/test_projects_handoffs.py +31 -0
- core/api/tests/test_providers.py +138 -0
- core/api/tests/test_safety_bridge.py +347 -0
- core/api/tests/test_session_catalog.py +142 -0
- core/api/tests/test_session_conversations.py +512 -0
- core/api/tests/test_session_metrics_service.py +270 -0
- core/api/tests/test_session_resume_paths.py +548 -0
- core/api/tests/test_session_theme_mode_migration.py +56 -0
- core/api/tests/test_sessions_rbac.py +131 -0
- core/api/tests/test_share_edit.py +398 -0
- core/api/tests/test_share_repo.py +200 -0
- core/api/tests/test_terminal_session_manager.py +98 -0
- core/api/tests/test_terminal_upload.py +34 -0
- core/api/tests/test_tmux.py +272 -0
- core/api/tests/test_workspace_sync.py +186 -0
- core/api/tests/test_ws_ticket_in_memory.py +73 -0
- core/api/use_cases/__init__.py +11 -0
- core/api/use_cases/_context.py +89 -0
- core/api/use_cases/_errors.py +62 -0
- core/api/use_cases/_roles.py +16 -0
- core/api/use_cases/audit.py +171 -0
- core/api/use_cases/brain.py +1232 -0
- core/api/use_cases/costs.py +249 -0
- core/api/use_cases/graph.py +1153 -0
- core/api/use_cases/handoffs.py +506 -0
- core/api/use_cases/ingest_triage.py +1229 -0
- core/api/use_cases/learnings.py +538 -0
- core/api/use_cases/projects.py +705 -0
- core/api/use_cases/pull_requests.py +415 -0
- core/api/use_cases/search.py +926 -0
- core/api/use_cases/tasks.py +1495 -0
- core/api/visibility.py +141 -0
- core/cli/__init__.py +5 -0
- core/cli/_index_source.py +632 -0
- core/cli/_runtime_ctx.py +160 -0
- core/cli/_transmute.py +241 -0
- core/cli/marvis_doctor.py +704 -0
- core/cli/marvis_feedback.py +396 -0
- core/cli/marvis_governance.py +315 -0
- core/cli/marvis_hooks.py +515 -0
- core/cli/marvis_init.py +757 -0
- core/cli/marvis_mcp.py +401 -0
- core/cli/marvis_runtime.py +855 -0
- core/cli/marvis_telemetry.py +228 -0
- core/scripts/_drift_check.py +716 -0
- core/scripts/_frontmatter.py +66 -0
- core/scripts/_graph_writer.py +189 -0
- core/scripts/ast_parser.py +1553 -0
- core/scripts/install_hooks/__init__.py +1 -0
- core/scripts/install_hooks/_config.sh +109 -0
- core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
- core/scripts/install_hooks/block-db-direct-write.sh +23 -0
- core/scripts/install_hooks/block-push-no-task.sh +23 -0
- core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
- core/scripts/install_hooks/block-subtree-push.sh +23 -0
- core/scripts/install_hooks/config.json +53 -0
- core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
- core/scripts/install_hooks/enforce-worktree.sh +23 -0
- core/scripts/install_hooks/quality-gate.sh +170 -0
- core/scripts/install_hooks/safety_bridge.py +968 -0
- core/scripts/install_hooks/secret-scan.sh +23 -0
- core/scripts/migrate_spike_node_ids.py +122 -0
- core/scripts/populate_artifacts.py +2198 -0
- core/scripts/populate_cross_project.py +2457 -0
- core/scripts/populate_inbox_nodes.py +357 -0
- core/scripts/populate_pr_impact.py +267 -0
- core/scripts/populate_project_nodes.py +603 -0
- core/scripts/populate_touch_counter.py +337 -0
- core/scripts/reparse_failed.py +57 -0
- core/scripts/safety_bridge.py +968 -0
- core/telemetry/__init__.py +9 -0
- core/telemetry/client.py +405 -0
- core/telemetry/schema.py +122 -0
- core/wizard/__init__.py +65 -0
- core/wizard/byok_vault.py +147 -0
- core/wizard/defaults.py +58 -0
- core/wizard/state.py +117 -0
- core/wizard/steps.py +70 -0
- core/wizard/validation.py +136 -0
- marvisx_cli-0.1.0.dist-info/METADATA +201 -0
- marvisx_cli-0.1.0.dist-info/RECORD +587 -0
- marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
- marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
- marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
- migrations/001_initial.sql +33 -0
- migrations/002_tasks.sql +30 -0
- migrations/003_session_management.sql +7 -0
- migrations/004_projects_comments.sql +65 -0
- migrations/005_session_intelligence.sql +15 -0
- migrations/006_settings.sql +12 -0
- migrations/007_task_scoring.sql +12 -0
- migrations/008_cost_tracking.sql +31 -0
- migrations/009_session_card_metrics.sql +3 -0
- migrations/010_monitoring.sql +55 -0
- migrations/012_agent_api.sql +21 -0
- migrations/013_session_complete.sql +8 -0
- migrations/015_pull_requests.sql +43 -0
- migrations/015_pull_requests_down.sql +5 -0
- migrations/016_users_raci.sql +116 -0
- migrations/017_task_cost_entries.sql +87 -0
- migrations/018_agents.sql +73 -0
- migrations/018_agents_down.sql +13 -0
- migrations/019_review_feedback.sql +18 -0
- migrations/020_pr_commit_sha.sql +4 -0
- migrations/021_webhook_events.sql +18 -0
- migrations/022_devx_agent_managed.sql +11 -0
- migrations/022_devx_agent_managed_down.sql +6 -0
- migrations/023_devx_p1_gate.sql +7 -0
- migrations/023_devx_p1_gate_down.sql +3 -0
- migrations/024_chat_messages.sql +16 -0
- migrations/024_pr_conversation_id.sql +8 -0
- migrations/024_task_indexes.sql +21 -0
- migrations/024_task_indexes_down.sql +7 -0
- migrations/025_audit_log.sql +17 -0
- migrations/026_agent_tokens.sql +20 -0
- migrations/027_teams_auth_phase_b.sql +35 -0
- migrations/028_learnings.sql +23 -0
- migrations/029_team_roles.sql +14 -0
- migrations/030_finder_pins.sql +10 -0
- migrations/031_pr_deploy_status.sql +9 -0
- migrations/032_task_reminders.sql +7 -0
- migrations/033_events_retry_count.sql +6 -0
- migrations/033_session_owner.sql +9 -0
- migrations/034_notifications.sql +38 -0
- migrations/035_shared_links.sql +15 -0
- migrations/036_session_index_upgrade.sql +29 -0
- migrations/037_pr_approval.sql +15 -0
- migrations/038_pr_submitted_by.sql +6 -0
- migrations/039_push_subscriptions.sql +17 -0
- migrations/040_semantic_search.sql +16 -0
- migrations/041_workspaces.sql +63 -0
- migrations/042_oidc_providers.sql +24 -0
- migrations/043_ci_checks.sql +31 -0
- migrations/044_agent_metrics.sql +30 -0
- migrations/045_documents_doc_type.sql +5 -0
- migrations/046_salience.sql +13 -0
- migrations/047_seed_missing_agents.sql +6 -0
- migrations/048_fix_agent_paths_roles.sql +5 -0
- migrations/049_agent_role_and_learnings_schema.sql +3 -0
- migrations/050_session_provider.sql +2 -0
- migrations/051_session_launch_profile.sql +4 -0
- migrations/052_session_theme_mode.sql +2 -0
- migrations/052_task_kind.sql +4 -0
- migrations/053_inbox_items.sql +31 -0
- migrations/054_inbox_triage_contract.sql +30 -0
- migrations/055_inbox_topic_treatment.sql +12 -0
- migrations/056_inbox_treatment_read_save.sql +57 -0
- migrations/057_session_theme_mode_backfill.sql +4 -0
- migrations/058_inbox_item_status_lifecycle.sql +13 -0
- migrations/059_inbox_tldr_and_source_scores.sql +18 -0
- migrations/060_newsletter.sql +16 -0
- migrations/061_inbox_redesign.sql +69 -0
- migrations/062_fix_inbox_sources_backfill.sql +37 -0
- migrations/063_task_completion_mode.sql +23 -0
- migrations/064_judge_mode_setting.sql +4 -0
- migrations/065_knowledge_graph_spike.sql +40 -0
- migrations/066_digest_ranking_inputs.sql +10 -0
- migrations/066_kg_artifact_nodes.sql +129 -0
- migrations/067_inbox_digest_selections.sql +28 -0
- migrations/067_kg_temporal.sql +53 -0
- migrations/068_inbox_digest_app_settings.sql +9 -0
- migrations/068_kg_touch_counter.sql +52 -0
- migrations/069_kg_doc_types.sql +117 -0
- migrations/070_digest_ranking_inputs_recovery.sql +3 -0
- migrations/071_inbox_digest_selections_recovery.sql +3 -0
- migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
- migrations/073_kg_cross_project.sql +216 -0
- migrations/073_kg_cross_project_down.sql +77 -0
- migrations/074_kg_infra_types.sql +208 -0
- migrations/074_kg_infra_types_down.sql +80 -0
- migrations/075_kg_file_state_recovery.sql +35 -0
- migrations/075_kg_file_state_recovery_down.sql +5 -0
- migrations/076_kg_watcher_state.sql +33 -0
- migrations/076_kg_watcher_state_down.sql +3 -0
- migrations/077_kg_doc_types_extend.sql +226 -0
- migrations/077_kg_doc_types_extend_down.sql +80 -0
- migrations/078_kg_fts5.sql +102 -0
- migrations/078_kg_fts5_down.sql +14 -0
- migrations/079_kg_missing_indexes.sql +31 -0
- migrations/079_kg_missing_indexes_down.sql +10 -0
- migrations/080_kg_fts5_extended.sql +232 -0
- migrations/080_kg_fts5_extended_down.sql +25 -0
- migrations/081_kg_lens_indexes.sql +9 -0
- migrations/081_kg_lens_indexes_down.sql +3 -0
- migrations/082_kg_pins.sql +26 -0
- migrations/082_kg_pins_down.sql +14 -0
- migrations/083_kg_graph_nodes_degree.sql +20 -0
- migrations/083_kg_graph_nodes_degree_down.sql +15 -0
- migrations/084_drop_legacy_scheduler_tables.sql +58 -0
- migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
- migrations/085_kg_edge_resolves_to.sql +142 -0
- migrations/085_kg_edge_resolves_to_down.sql +66 -0
- migrations/086_project_status_updates_feed.sql +20 -0
- migrations/086_project_status_updates_feed_down.sql +36 -0
- migrations/087_session_metrics_dual.sql +50 -0
- migrations/087_session_metrics_dual_down.sql +21 -0
- migrations/088_rename_context_pct_legacy.sql +23 -0
- migrations/088_rename_context_pct_legacy_down.sql +8 -0
- migrations/089_session_metrics_equivalent_cost.sql +26 -0
- migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
- migrations/090_kg_inbox_node_type.sql +26 -0
- migrations/090_kg_inbox_node_type_down.sql +20 -0
- migrations/091_kg_inbox_node_type_check.sql +265 -0
- migrations/091_kg_inbox_node_type_check_down.sql +129 -0
- migrations/092_sessions_activity_state_ts.sql +29 -0
- migrations/092_sessions_activity_state_ts_down.sql +14 -0
- migrations/093_sessions_activity_state_column.sql +29 -0
- migrations/093_sessions_activity_state_column_down.sql +10 -0
- migrations/094_ingest_pending.sql +55 -0
- migrations/094_ingest_pending_down.sql +15 -0
- migrations/095_kg_intent_first.sql +77 -0
- migrations/095_kg_intent_first_down.sql +25 -0
- migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
- migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
- migrations/097_ingest_change_history.sql +37 -0
- migrations/097_ingest_change_history_down.sql +13 -0
- migrations/098_kg_node_type_business.sql +254 -0
- migrations/098_kg_node_type_business_down.sql +195 -0
- migrations/099_kg_edges_restore_weight.sql +58 -0
- migrations/099_kg_edges_restore_weight_down.sql +12 -0
- migrations/100_kg_enriched_at.sql +25 -0
- migrations/100_kg_enriched_at_down.sql +12 -0
- migrations/101_local_llm_shadow_comparisons.sql +66 -0
- migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
- migrations/102_promote_llm_costs.sql +69 -0
- migrations/102_promote_llm_costs_down.sql +19 -0
- migrations/103_ingest_skipped_log.sql +46 -0
- migrations/103_ingest_skipped_log_down.sql +15 -0
- migrations/120_docs_governance.sql +50 -0
- migrations/120_docs_governance_down.sql +11 -0
- migrations/121_notification_event_fk_cleanup.sql +21 -0
- migrations/121_notification_event_fk_cleanup_down.sql +10 -0
- migrations/122_docs_drift_history.sql +34 -0
- migrations/122_docs_drift_history_down.sql +15 -0
- migrations/123_ingest_parser_waiting_status.sql +69 -0
- migrations/123_ingest_parser_waiting_status_down.sql +69 -0
- migrations/124_heypocket_recordings.sql +63 -0
- migrations/124_heypocket_recordings_down.sql +13 -0
- migrations/125_kg_node_type_record.sql +219 -0
- migrations/125_kg_node_type_record_down.sql +205 -0
- migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
- migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
- migrations/127_brain_v1_substrate.sql +200 -0
- migrations/127_brain_v1_substrate_down.sql +32 -0
- migrations/128_brain_drift_signals.sql +157 -0
- migrations/128_brain_drift_signals_down.sql +23 -0
- migrations/129_brain_memory_operations.sql +232 -0
- migrations/129_brain_memory_operations_down.sql +27 -0
- migrations/130_brain_findings.sql +258 -0
- migrations/130_brain_findings_down.sql +29 -0
- migrations/132_kg_pr_modifies.sql +242 -0
- migrations/132_kg_pr_modifies_down.sql +99 -0
- migrations/133_brain_v1_2_direction_schema.sql +476 -0
- migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
- migrations/134_brain_journal_narrative_polished.sql +8 -0
- migrations/134_brain_journal_narrative_polished_down.sql +6 -0
- migrations/135_kg_edges_provider.sql +21 -0
- migrations/136_documents_fts.sql +56 -0
- migrations/137_promote_llm_costs.sql +59 -0
- migrations/137_promote_llm_costs_down.sql +19 -0
- migrations/138_ingest_api_keys.sql +39 -0
- migrations/138_ingest_api_keys_down.sql +8 -0
- migrations/139_ingest_pending_ingress.sql +91 -0
- migrations/139_ingest_pending_ingress_down.sql +73 -0
- migrations/140_ingest_idempotency_quota.sql +45 -0
- migrations/140_ingest_idempotency_quota_down.sql +9 -0
- migrations/141_ingest_pending_metadata.sql +16 -0
- migrations/141_ingest_pending_metadata_down.sql +7 -0
- migrations/142_llm_function_config.sql +36 -0
- migrations/142_llm_function_config_down.sql +8 -0
- migrations/143_kg_code_embeddings.sql +25 -0
- migrations/143_kg_code_embeddings_down.sql +5 -0
- migrations/__init__.py +4 -0
- projects/_template/project.yaml +46 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# v2.0.0 - 2026-05-27 - S1 F1.7: thin adapter over use_cases.pull_requests (merge human-gate kept at Depends)
|
|
2
|
+
# v1.7.0 - 2026-03-11 - Fase 2: approve and request-changes endpoints (four-eyes gate)
|
|
3
|
+
"""HTTP adapter for the pull-requests domain (S1 collapse-runtime).
|
|
4
|
+
|
|
5
|
+
This router is a thin transport adapter. All branch/PR lifecycle + RBAC +
|
|
6
|
+
error-translation logic lives in :mod:`core.api.use_cases.pull_requests` (pure,
|
|
7
|
+
fastapi-free). Each handler resolves identity into a :class:`CallerContext`, calls
|
|
8
|
+
the use_case inside ``try/except`` -> ``to_http``, and owns the transport concerns.
|
|
9
|
+
|
|
10
|
+
MERGE HUMAN-GATE — kept at the ``Depends`` layer. ``POST /{task_id}/merge`` and
|
|
11
|
+
``/{task_id}/revert`` keep ``Depends(require_role(..., human_only=True))``.
|
|
12
|
+
``human_only=True`` swaps the auth dependency to cookie-only ``get_current_user``,
|
|
13
|
+
so a Bearer-only agent is rejected with ``401 Not authenticated`` upstream of the
|
|
14
|
+
handler (pinned by the merge-gate regression test ``test_br03_merge_pr_blocked_without_cookie``).
|
|
15
|
+
A use_case receiving a ``ctx`` cannot reproduce that 401, so the gate is NOT moved
|
|
16
|
+
into the domain — it stays exactly where it fires today.
|
|
17
|
+
|
|
18
|
+
ERROR BOUNDARIES kept in the adapter (legacy body parity):
|
|
19
|
+
* ``MergeConflictError`` (raised inside ``pr_service`` -> propagates through the
|
|
20
|
+
use_case) -> the exact legacy ``HTTPException(409, {message, conflicting_files})``
|
|
21
|
+
dict body. ``to_http`` would drop ``conflicting_files``.
|
|
22
|
+
* plain ``GitOpsError`` -> legacy ``HTTPException(500, str(exc))`` (pinned by
|
|
23
|
+
``tests/test_db_contention_task_flows.py::test_merge_push_failure_*`` which
|
|
24
|
+
asserts the raw message in the 500 body). ``to_http``'s structured dict would
|
|
25
|
+
break that substring assertion.
|
|
26
|
+
* ``HTTPException``s raised inside ``pr_service`` (title/migration/push paths)
|
|
27
|
+
propagate natively — Fase-2 conversion, out of scope here.
|
|
28
|
+
Domain :class:`ServiceError`s (404/409/422/403 the use_case raises) flow through
|
|
29
|
+
``to_http`` as the structured ``{code,message}`` body — their tests check status.
|
|
30
|
+
|
|
31
|
+
STAYS IN THE ADAPTER (transport concerns):
|
|
32
|
+
* ``get_pull_request`` ``deep`` KG enrichment (rate-limit + access log +
|
|
33
|
+
``build_kg_context_for_pr``); ``core.api.services.kg.audit`` imports ``fastapi``
|
|
34
|
+
so this MUST NOT touch the use_case (DECISION 2).
|
|
35
|
+
* visibility resolution on ``get_merge_conflicts`` (``get_visible_projects`` needs
|
|
36
|
+
``UserInfo.teams``) — resolved here, passed in (DECISION 1).
|
|
37
|
+
* approve/request-changes pass the full ``UserInfo`` (``user_id`` + team lookups)
|
|
38
|
+
through to the use_case, which forwards it to ``pr_service``.
|
|
39
|
+
|
|
40
|
+
``MergeConflictResponse`` (response DTO) is imported from ``core.api.models``
|
|
41
|
+
(unchanged, like costs/tasks). REQUEST models stay defined here.
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import logging
|
|
46
|
+
|
|
47
|
+
import aiosqlite
|
|
48
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
49
|
+
from pydantic import BaseModel
|
|
50
|
+
|
|
51
|
+
from core.api.db import get_db, get_write_db
|
|
52
|
+
from core.api.models import MergeConflictResponse, UserInfo
|
|
53
|
+
from core.api.rbac import require_role
|
|
54
|
+
from core.api.routers._adapter import to_http
|
|
55
|
+
from core.api.security import get_current_user_or_agent, require_any_auth
|
|
56
|
+
from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
|
|
57
|
+
from core.api.services.kg.lens import build_kg_context_for_pr
|
|
58
|
+
from core.api.use_cases import pull_requests as uc
|
|
59
|
+
from core.api.use_cases._context import CallerContext
|
|
60
|
+
from core.api.use_cases._errors import ServiceError
|
|
61
|
+
from core.api.visibility import get_visible_projects
|
|
62
|
+
|
|
63
|
+
# Re-export the git-ops error classes from the use_case so existing importers of
|
|
64
|
+
# ``core.api.routers.pull_requests.GitOpsError`` / ``MergeConflictError`` keep
|
|
65
|
+
# working unchanged.
|
|
66
|
+
from core.api.use_cases.pull_requests import ( # noqa: F401 (re-export surface)
|
|
67
|
+
GitOpsError,
|
|
68
|
+
MergeConflictError,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
logger = logging.getLogger(__name__)
|
|
72
|
+
|
|
73
|
+
router = APIRouter(prefix="/api/v1/pull_requests", tags=["pull-requests"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- Request models (kept in the adapter) ---
|
|
77
|
+
|
|
78
|
+
class SubmitPrRequest(BaseModel):
|
|
79
|
+
title: str
|
|
80
|
+
body: str = ""
|
|
81
|
+
|
|
82
|
+
class ClosePrRequest(BaseModel):
|
|
83
|
+
reason: str = ""
|
|
84
|
+
|
|
85
|
+
class UpdatePrRequest(BaseModel):
|
|
86
|
+
title: str | None = None
|
|
87
|
+
body: str | None = None
|
|
88
|
+
|
|
89
|
+
class RegisterBranchRequest(BaseModel):
|
|
90
|
+
branch_name: str
|
|
91
|
+
worktree_path: str | None = None
|
|
92
|
+
|
|
93
|
+
class RequestChangesBody(BaseModel):
|
|
94
|
+
comment: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- Endpoints (thin adapters) ---
|
|
98
|
+
|
|
99
|
+
@router.get("/merge-conflicts", response_model=MergeConflictResponse)
|
|
100
|
+
async def get_merge_conflicts(
|
|
101
|
+
project: str = Query(..., description="Project slug to check for migration conflicts"),
|
|
102
|
+
user: UserInfo = Depends(require_any_auth),
|
|
103
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
104
|
+
):
|
|
105
|
+
"""Detect migration number conflicts across open PRs for a project."""
|
|
106
|
+
# DECISION 1: resolve visibility at the boundary, enforce in the use_case
|
|
107
|
+
# (404 on a non-visible slug — does not reveal existence).
|
|
108
|
+
visible_projects = await get_visible_projects(db, user)
|
|
109
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
110
|
+
try:
|
|
111
|
+
conflicts = await uc.get_merge_conflicts(
|
|
112
|
+
ctx, db, project=project, visible_projects=visible_projects
|
|
113
|
+
)
|
|
114
|
+
return MergeConflictResponse(conflicts=conflicts)
|
|
115
|
+
except ServiceError as e:
|
|
116
|
+
raise to_http(e)
|
|
117
|
+
except GitOpsError as exc:
|
|
118
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.post("/{task_id}/branch")
|
|
122
|
+
async def create_branch(
|
|
123
|
+
task_id: str,
|
|
124
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
125
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
126
|
+
):
|
|
127
|
+
"""Create git worktree and branch for a task. Idempotent."""
|
|
128
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
129
|
+
try:
|
|
130
|
+
return await uc.create_branch(ctx, db, task_id=task_id)
|
|
131
|
+
except ServiceError as e:
|
|
132
|
+
raise to_http(e)
|
|
133
|
+
except GitOpsError as exc:
|
|
134
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.post("/{task_id}/register")
|
|
138
|
+
async def register_branch(
|
|
139
|
+
task_id: str,
|
|
140
|
+
body: RegisterBranchRequest,
|
|
141
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
142
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
143
|
+
):
|
|
144
|
+
"""Register an externally-created branch as a draft PR record.
|
|
145
|
+
|
|
146
|
+
Used when an agent creates a worktree via git directly (not via /branch endpoint).
|
|
147
|
+
Idempotent: returns existing active PR if one already exists.
|
|
148
|
+
"""
|
|
149
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
150
|
+
try:
|
|
151
|
+
return await uc.register_branch(
|
|
152
|
+
ctx, db, task_id=task_id, branch_name=body.branch_name,
|
|
153
|
+
worktree_path=body.worktree_path,
|
|
154
|
+
)
|
|
155
|
+
except ServiceError as e:
|
|
156
|
+
raise to_http(e)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@router.get("/{task_id}")
|
|
160
|
+
async def get_pull_request(
|
|
161
|
+
task_id: str,
|
|
162
|
+
deep: bool = Query(False),
|
|
163
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
164
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
165
|
+
):
|
|
166
|
+
"""Get PR status and diff for a task."""
|
|
167
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
168
|
+
try:
|
|
169
|
+
result = await uc.get_pull_request(ctx, db, task_id=task_id)
|
|
170
|
+
except ServiceError as e:
|
|
171
|
+
raise to_http(e)
|
|
172
|
+
|
|
173
|
+
# DECISION 2: deep KG enrichment is a per-surface adapter concern.
|
|
174
|
+
if deep:
|
|
175
|
+
check_deep_rate_limit(user.username)
|
|
176
|
+
log_kg_deep_access(user.username, "get_pull_request", task_id)
|
|
177
|
+
result["kg_context"] = await build_kg_context_for_pr(db, task_id, deep=True)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.post("/{task_id}/submit")
|
|
182
|
+
async def submit_pull_request(
|
|
183
|
+
task_id: str,
|
|
184
|
+
body: SubmitPrRequest,
|
|
185
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
186
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
187
|
+
):
|
|
188
|
+
"""Submit branch for review. Moves PR from draft to open."""
|
|
189
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
190
|
+
try:
|
|
191
|
+
return await uc.submit_pull_request(
|
|
192
|
+
ctx, db, task_id=task_id, title=body.title, body=body.body
|
|
193
|
+
)
|
|
194
|
+
except ServiceError as e:
|
|
195
|
+
raise to_http(e)
|
|
196
|
+
except GitOpsError as exc:
|
|
197
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@router.post("/{task_id}/merge")
|
|
201
|
+
async def merge_pull_request(
|
|
202
|
+
task_id: str,
|
|
203
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin", human_only=True)),
|
|
204
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
205
|
+
):
|
|
206
|
+
"""Merge PR. Human operator+ authorization required (no agent tokens).
|
|
207
|
+
|
|
208
|
+
The human-only gate is the ``human_only=True`` dependency above (cookie-only
|
|
209
|
+
auth) — it fires upstream of this handler and is NOT duplicated in the use_case.
|
|
210
|
+
"""
|
|
211
|
+
ctx = CallerContext.from_user_info(user, is_human_session=True)
|
|
212
|
+
try:
|
|
213
|
+
return await uc.merge_pull_request(ctx, db, task_id=task_id)
|
|
214
|
+
except MergeConflictError as exc:
|
|
215
|
+
# Preserve the exact legacy 409 dict body (message + conflicting_files).
|
|
216
|
+
raise HTTPException(
|
|
217
|
+
status_code=409,
|
|
218
|
+
detail={
|
|
219
|
+
"message": (
|
|
220
|
+
"Merge conflict: cannot fast-forward or 3-way merge the PR branch into main. "
|
|
221
|
+
"Reason: main has changes that touch the same lines as this PR since the branch diverged. "
|
|
222
|
+
"Fix: (1) checkout the PR branch in its worktree, (2) `git fetch origin && git rebase origin/main`, "
|
|
223
|
+
"(3) resolve conflicts in the files listed below, (4) push the rebased branch, (5) retry merge."
|
|
224
|
+
),
|
|
225
|
+
"conflicting_files": exc.conflicting_files,
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
except GitOpsError as exc:
|
|
229
|
+
# Plain GitOpsError -> legacy 500 plain-string body.
|
|
230
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
231
|
+
except ServiceError as e:
|
|
232
|
+
raise to_http(e)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@router.post("/{task_id}/approve")
|
|
236
|
+
async def approve_pull_request(
|
|
237
|
+
task_id: str,
|
|
238
|
+
user: UserInfo = Depends(require_role("admin", "super_admin", "operator")),
|
|
239
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
240
|
+
):
|
|
241
|
+
"""Approva PR. Richiede team_admin del progetto o admin+. Four-eyes: non puoi approvare la tua PR."""
|
|
242
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
243
|
+
try:
|
|
244
|
+
return await uc.approve_pull_request(ctx, db, task_id=task_id, reviewer=user)
|
|
245
|
+
except ServiceError as e:
|
|
246
|
+
raise to_http(e)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@router.post("/{task_id}/request-changes")
|
|
250
|
+
async def request_pr_changes(
|
|
251
|
+
task_id: str,
|
|
252
|
+
body: RequestChangesBody,
|
|
253
|
+
user: UserInfo = Depends(require_role("admin", "super_admin", "operator")),
|
|
254
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
255
|
+
):
|
|
256
|
+
"""Request changes. Revoca approvazione e rimanda task in_progress."""
|
|
257
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
258
|
+
try:
|
|
259
|
+
return await uc.request_pull_request_changes(
|
|
260
|
+
ctx, db, task_id=task_id, reviewer=user, comment=body.comment
|
|
261
|
+
)
|
|
262
|
+
except ServiceError as e:
|
|
263
|
+
raise to_http(e)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@router.post("/{task_id}/close")
|
|
267
|
+
async def close_pull_request(
|
|
268
|
+
task_id: str,
|
|
269
|
+
body: ClosePrRequest,
|
|
270
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
271
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
272
|
+
):
|
|
273
|
+
"""Close PR without merge. Task returns to in_progress."""
|
|
274
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
275
|
+
try:
|
|
276
|
+
return await uc.close_pull_request(ctx, db, task_id=task_id, reason=body.reason)
|
|
277
|
+
except ServiceError as e:
|
|
278
|
+
raise to_http(e)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@router.patch("/{task_id}")
|
|
282
|
+
async def update_pull_request(
|
|
283
|
+
task_id: str,
|
|
284
|
+
body: UpdatePrRequest,
|
|
285
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
286
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
287
|
+
):
|
|
288
|
+
"""Update PR title or body."""
|
|
289
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
290
|
+
try:
|
|
291
|
+
return await uc.update_pull_request(
|
|
292
|
+
ctx, db, task_id=task_id, title=body.title, body=body.body
|
|
293
|
+
)
|
|
294
|
+
except ServiceError as e:
|
|
295
|
+
raise to_http(e)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@router.post("/{task_id}/revert")
|
|
299
|
+
async def revert_pull_request(
|
|
300
|
+
task_id: str,
|
|
301
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin", human_only=True)),
|
|
302
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
303
|
+
):
|
|
304
|
+
"""Revert a merged PR. Creates a new task + new PR with git revert commit.
|
|
305
|
+
Human operator+ authorization required (cookie-only ``human_only`` dependency)."""
|
|
306
|
+
ctx = CallerContext.from_user_info(user, is_human_session=True)
|
|
307
|
+
try:
|
|
308
|
+
return await uc.revert_pull_request(ctx, db, task_id=task_id)
|
|
309
|
+
except ServiceError as e:
|
|
310
|
+
raise to_http(e)
|
|
311
|
+
except GitOpsError as exc:
|
|
312
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
core/api/routers/push.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# v1.0.0 - 2026-03-13 - Push subscription management (POST/DELETE)
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import aiosqlite
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from core.api.db import get_db, get_write_db
|
|
9
|
+
from core.api.models import UserInfo
|
|
10
|
+
from core.api.rbac import require_role
|
|
11
|
+
from core.api.services.push_service import MAX_SUBSCRIPTIONS_PER_USER, validate_push_endpoint
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/api/v1/push-subscriptions", tags=["push"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SubscribeRequest(BaseModel):
|
|
17
|
+
endpoint: str
|
|
18
|
+
p256dh: str
|
|
19
|
+
auth: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnsubscribeRequest(BaseModel):
|
|
23
|
+
endpoint: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post("", status_code=201)
|
|
27
|
+
async def subscribe(
|
|
28
|
+
body: SubscribeRequest,
|
|
29
|
+
user: UserInfo = Depends(require_role("operator", human_only=True)),
|
|
30
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Register a push subscription for the current user."""
|
|
33
|
+
if not validate_push_endpoint(body.endpoint):
|
|
34
|
+
raise HTTPException(400, "Invalid push endpoint")
|
|
35
|
+
|
|
36
|
+
# Cap subscriptions per user
|
|
37
|
+
cursor = await db.execute(
|
|
38
|
+
"SELECT COUNT(*) FROM push_subscriptions WHERE user_id = ?",
|
|
39
|
+
[user.user_id],
|
|
40
|
+
)
|
|
41
|
+
row = await cursor.fetchone()
|
|
42
|
+
if row[0] >= MAX_SUBSCRIPTIONS_PER_USER:
|
|
43
|
+
raise HTTPException(400, f"Max {MAX_SUBSCRIPTIONS_PER_USER} subscriptions per user")
|
|
44
|
+
|
|
45
|
+
await db.execute(
|
|
46
|
+
"INSERT OR REPLACE INTO push_subscriptions (endpoint, user_id, p256dh, auth) VALUES (?, ?, ?, ?)",
|
|
47
|
+
[body.endpoint, user.user_id, body.p256dh, body.auth],
|
|
48
|
+
)
|
|
49
|
+
await db.commit()
|
|
50
|
+
return {"status": "subscribed"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.delete("")
|
|
54
|
+
async def unsubscribe(
|
|
55
|
+
body: UnsubscribeRequest,
|
|
56
|
+
user: UserInfo = Depends(require_role("operator", human_only=True)),
|
|
57
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Remove a push subscription for the current user."""
|
|
60
|
+
result = await db.execute(
|
|
61
|
+
"DELETE FROM push_subscriptions WHERE endpoint = ? AND user_id = ?",
|
|
62
|
+
[body.endpoint, user.user_id],
|
|
63
|
+
)
|
|
64
|
+
await db.commit()
|
|
65
|
+
if result.rowcount == 0:
|
|
66
|
+
raise HTTPException(404, "Subscription not found")
|
|
67
|
+
return {"status": "unsubscribed"}
|
core/api/routers/raci.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# v1.3.0 - 2026-03-10 - Remove hardcoded _SYSTEM_ACTOR, use authenticated user for attribution
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
import aiosqlite
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
+
|
|
10
|
+
from core.api.db import get_db, get_write_db
|
|
11
|
+
from core.api.models import RaciEntry, RaciAddRequest, RaciReplaceRequest, UserInfo, UserSummary
|
|
12
|
+
from core.api.rbac import require_role
|
|
13
|
+
from core.api.security import get_current_user, get_current_user_or_agent
|
|
14
|
+
from core.api.services.events import emit_event
|
|
15
|
+
from core.api.visibility import check_project_access
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
router = APIRouter(prefix="/api/v1/projects", tags=["raci"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _get_raci_with_users(
|
|
22
|
+
db: aiosqlite.Connection, project: str
|
|
23
|
+
) -> list[RaciEntry]:
|
|
24
|
+
"""Fetch RACI entries with embedded user summaries, excluding soft-deleted users."""
|
|
25
|
+
db.row_factory = aiosqlite.Row
|
|
26
|
+
async with db.execute(
|
|
27
|
+
"""
|
|
28
|
+
SELECT r.role, u.id, u.slug, u.display_name, u.avatar_color
|
|
29
|
+
FROM project_raci r
|
|
30
|
+
JOIN users u ON u.id = r.user_id
|
|
31
|
+
WHERE r.project = ? AND u.deleted_at IS NULL
|
|
32
|
+
ORDER BY
|
|
33
|
+
CASE r.role
|
|
34
|
+
WHEN 'responsible' THEN 1
|
|
35
|
+
WHEN 'accountable' THEN 2
|
|
36
|
+
WHEN 'consulted' THEN 3
|
|
37
|
+
WHEN 'informed' THEN 4
|
|
38
|
+
END,
|
|
39
|
+
u.display_name
|
|
40
|
+
""",
|
|
41
|
+
(project,),
|
|
42
|
+
) as cursor:
|
|
43
|
+
rows = await cursor.fetchall()
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
RaciEntry(
|
|
47
|
+
user=UserSummary(
|
|
48
|
+
id=r["id"],
|
|
49
|
+
slug=r["slug"],
|
|
50
|
+
display_name=r["display_name"],
|
|
51
|
+
avatar_color=r["avatar_color"] or "#6366f1",
|
|
52
|
+
),
|
|
53
|
+
role=r["role"],
|
|
54
|
+
)
|
|
55
|
+
for r in rows
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/{slug}/raci", response_model=list[RaciEntry])
|
|
60
|
+
async def get_raci(
|
|
61
|
+
slug: str,
|
|
62
|
+
user=Depends(get_current_user_or_agent),
|
|
63
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
64
|
+
):
|
|
65
|
+
"""Lista RACI del progetto con dati utente embedded."""
|
|
66
|
+
await check_project_access(slug, user, db)
|
|
67
|
+
return await _get_raci_with_users(db, slug)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.post("/{slug}/raci", response_model=list[RaciEntry], status_code=201)
|
|
71
|
+
async def add_raci_entry(
|
|
72
|
+
slug: str,
|
|
73
|
+
body: RaciAddRequest,
|
|
74
|
+
user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
|
|
75
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
76
|
+
):
|
|
77
|
+
"""Aggiungi singola entry RACI.
|
|
78
|
+
|
|
79
|
+
Fallisce con 409 se Responsible o Accountable sono gia presenti nel progetto
|
|
80
|
+
(partial unique index garantisce uno solo per progetto).
|
|
81
|
+
"""
|
|
82
|
+
db.row_factory = aiosqlite.Row
|
|
83
|
+
|
|
84
|
+
# Verifica utente esiste e non e eliminato
|
|
85
|
+
u = await (
|
|
86
|
+
await db.execute(
|
|
87
|
+
"SELECT id FROM users WHERE id = ? AND deleted_at IS NULL",
|
|
88
|
+
(body.user_id,),
|
|
89
|
+
)
|
|
90
|
+
).fetchone()
|
|
91
|
+
if not u:
|
|
92
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
93
|
+
|
|
94
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
await db.execute(
|
|
98
|
+
"INSERT INTO project_raci (project, user_id, role) VALUES (?, ?, ?)",
|
|
99
|
+
(slug, body.user_id, body.role),
|
|
100
|
+
)
|
|
101
|
+
await db.execute(
|
|
102
|
+
"INSERT INTO project_raci_history "
|
|
103
|
+
"(project, user_id, role, action, changed_by, changed_at, reason) "
|
|
104
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
105
|
+
(slug, body.user_id, body.role, "assign", user.user_id or user.username, now, body.reason),
|
|
106
|
+
)
|
|
107
|
+
await db.commit()
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
if "UNIQUE" in str(exc):
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=409,
|
|
112
|
+
detail=f"Conflict: role '{body.role}' already assigned in project '{slug}'",
|
|
113
|
+
)
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
await emit_event(
|
|
117
|
+
db,
|
|
118
|
+
"raci.updated",
|
|
119
|
+
project=slug,
|
|
120
|
+
target_type="raci",
|
|
121
|
+
target_id=slug,
|
|
122
|
+
payload={"action": "assign", "user_id": body.user_id, "role": body.role},
|
|
123
|
+
)
|
|
124
|
+
return await _get_raci_with_users(db, slug)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.put("/{slug}/raci", response_model=list[RaciEntry])
|
|
128
|
+
async def replace_raci(
|
|
129
|
+
slug: str,
|
|
130
|
+
body: RaciReplaceRequest,
|
|
131
|
+
user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
|
|
132
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
133
|
+
):
|
|
134
|
+
"""Replace completo RACI del progetto (idempotente).
|
|
135
|
+
|
|
136
|
+
Revoca tutte le entry esistenti e reinserisce quelle fornite.
|
|
137
|
+
Tutto registrato in project_raci_history per audit trail.
|
|
138
|
+
"""
|
|
139
|
+
await check_project_access(slug, user, db)
|
|
140
|
+
db.row_factory = aiosqlite.Row
|
|
141
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
142
|
+
|
|
143
|
+
# Leggi entries esistenti per audit trail
|
|
144
|
+
async with db.execute(
|
|
145
|
+
"SELECT user_id, role FROM project_raci WHERE project = ?", (slug,)
|
|
146
|
+
) as cursor:
|
|
147
|
+
existing = await cursor.fetchall()
|
|
148
|
+
|
|
149
|
+
# Cancella entries correnti
|
|
150
|
+
await db.execute("DELETE FROM project_raci WHERE project = ?", (slug,))
|
|
151
|
+
|
|
152
|
+
# Registra revoca per audit
|
|
153
|
+
for row in existing:
|
|
154
|
+
await db.execute(
|
|
155
|
+
"INSERT INTO project_raci_history "
|
|
156
|
+
"(project, user_id, role, action, changed_by, changed_at) "
|
|
157
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
158
|
+
(slug, row["user_id"], row["role"], "revoke", user.user_id or user.username, now),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Reinserisci nuove entries
|
|
162
|
+
for entry in body.entries:
|
|
163
|
+
await db.execute(
|
|
164
|
+
"INSERT OR IGNORE INTO project_raci (project, user_id, role) VALUES (?, ?, ?)",
|
|
165
|
+
(slug, entry.user_id, entry.role),
|
|
166
|
+
)
|
|
167
|
+
await db.execute(
|
|
168
|
+
"INSERT INTO project_raci_history "
|
|
169
|
+
"(project, user_id, role, action, changed_by, changed_at, reason) "
|
|
170
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
171
|
+
(
|
|
172
|
+
slug,
|
|
173
|
+
entry.user_id,
|
|
174
|
+
entry.role,
|
|
175
|
+
"assign",
|
|
176
|
+
user.user_id or user.username,
|
|
177
|
+
now,
|
|
178
|
+
entry.reason,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
await db.commit()
|
|
183
|
+
|
|
184
|
+
await emit_event(
|
|
185
|
+
db,
|
|
186
|
+
"raci.updated",
|
|
187
|
+
project=slug,
|
|
188
|
+
target_type="raci",
|
|
189
|
+
target_id=slug,
|
|
190
|
+
payload={"action": "replace", "entries": len(body.entries)},
|
|
191
|
+
)
|
|
192
|
+
return await _get_raci_with_users(db, slug)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.delete("/{slug}/raci/{user_id}/{role}", status_code=204)
|
|
196
|
+
async def remove_raci_entry(
|
|
197
|
+
slug: str,
|
|
198
|
+
user_id: str,
|
|
199
|
+
role: str,
|
|
200
|
+
user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
|
|
201
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
202
|
+
):
|
|
203
|
+
"""Rimuovi singola entry RACI. Registra revoca in audit trail."""
|
|
204
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
205
|
+
|
|
206
|
+
result = await db.execute(
|
|
207
|
+
"DELETE FROM project_raci WHERE project = ? AND user_id = ? AND role = ?",
|
|
208
|
+
(slug, user_id, role),
|
|
209
|
+
)
|
|
210
|
+
if result.rowcount == 0:
|
|
211
|
+
raise HTTPException(status_code=404, detail="RACI entry not found")
|
|
212
|
+
|
|
213
|
+
await db.execute(
|
|
214
|
+
"INSERT INTO project_raci_history "
|
|
215
|
+
"(project, user_id, role, action, changed_by, changed_at) "
|
|
216
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
217
|
+
(slug, user_id, role, "revoke", user.user_id or user.username, now),
|
|
218
|
+
)
|
|
219
|
+
await db.commit()
|
|
220
|
+
|
|
221
|
+
await emit_event(
|
|
222
|
+
db,
|
|
223
|
+
"raci.updated",
|
|
224
|
+
project=slug,
|
|
225
|
+
target_type="raci",
|
|
226
|
+
target_id=slug,
|
|
227
|
+
payload={"action": "revoke", "user_id": user_id, "role": role},
|
|
228
|
+
)
|