attocode-code-intel 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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Code analysis endpoints — unified v1 (text) + v2 (structured JSON)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, Query
|
|
8
|
+
|
|
9
|
+
from attocode_intel.api.auth import verify_auth
|
|
10
|
+
from attocode_intel.api.deps import (
|
|
11
|
+
BranchParam,
|
|
12
|
+
ensure_branch_supported,
|
|
13
|
+
get_analysis_provider,
|
|
14
|
+
get_service_or_404,
|
|
15
|
+
)
|
|
16
|
+
from attocode_intel.api.models import (
|
|
17
|
+
BootstrapRequest,
|
|
18
|
+
BugScanRequest,
|
|
19
|
+
ConventionsResponse,
|
|
20
|
+
CrossRefResponse,
|
|
21
|
+
DeadCodeRequest,
|
|
22
|
+
DependencyGraphRequest,
|
|
23
|
+
DependencyGraphResponse,
|
|
24
|
+
DependencyResponse,
|
|
25
|
+
DistillRequest,
|
|
26
|
+
ExploreRequest,
|
|
27
|
+
FileAnalysisResponse,
|
|
28
|
+
HotspotsResponse,
|
|
29
|
+
ImpactAnalysisResponse,
|
|
30
|
+
NotifyFilesRequest,
|
|
31
|
+
ReadinessReportRequest,
|
|
32
|
+
RepoMapRankedRequest,
|
|
33
|
+
AnalyzeRequest,
|
|
34
|
+
ListRulesRequest,
|
|
35
|
+
RegisterRuleRequest,
|
|
36
|
+
SecurityScanRequest,
|
|
37
|
+
SymbolListResponse,
|
|
38
|
+
SymbolSearchResponse,
|
|
39
|
+
TextResult,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# --- v1 router: text responses (MCP-compatible) ---
|
|
43
|
+
|
|
44
|
+
router_v1 = APIRouter(
|
|
45
|
+
prefix="/api/v1/projects/{project_id}",
|
|
46
|
+
tags=["analysis"],
|
|
47
|
+
dependencies=[Depends(verify_auth)],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# --- v2 router: structured JSON responses ---
|
|
51
|
+
|
|
52
|
+
router_v2 = APIRouter(
|
|
53
|
+
prefix="/api/v2/projects/{project_id}",
|
|
54
|
+
tags=["analysis-v2"],
|
|
55
|
+
dependencies=[Depends(verify_auth)],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ===================================================================
|
|
60
|
+
# v1 endpoints — text output, call CodeIntelService directly
|
|
61
|
+
# ===================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router_v1.get("/map", response_model=TextResult)
|
|
65
|
+
async def repo_map(
|
|
66
|
+
project_id: str,
|
|
67
|
+
branch: BranchParam = "",
|
|
68
|
+
include_symbols: bool = True,
|
|
69
|
+
max_tokens: int = 6000,
|
|
70
|
+
) -> TextResult:
|
|
71
|
+
"""Get token-budgeted repository map."""
|
|
72
|
+
ensure_branch_supported(branch)
|
|
73
|
+
svc = await get_service_or_404(project_id)
|
|
74
|
+
return TextResult(result=svc.repo_map(include_symbols=include_symbols, max_tokens=max_tokens))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router_v1.get("/summary", response_model=TextResult)
|
|
78
|
+
async def project_summary(
|
|
79
|
+
project_id: str,
|
|
80
|
+
branch: BranchParam = "",
|
|
81
|
+
max_tokens: int = 4000,
|
|
82
|
+
) -> TextResult:
|
|
83
|
+
"""Get high-level project overview."""
|
|
84
|
+
ensure_branch_supported(branch)
|
|
85
|
+
svc = await get_service_or_404(project_id)
|
|
86
|
+
return TextResult(result=svc.project_summary(max_tokens=max_tokens))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router_v1.post("/bootstrap", response_model=TextResult)
|
|
90
|
+
async def bootstrap(
|
|
91
|
+
project_id: str,
|
|
92
|
+
req: BootstrapRequest,
|
|
93
|
+
branch: BranchParam = "",
|
|
94
|
+
) -> TextResult:
|
|
95
|
+
"""All-in-one codebase orientation."""
|
|
96
|
+
ensure_branch_supported(branch)
|
|
97
|
+
svc = await get_service_or_404(project_id)
|
|
98
|
+
return TextResult(result=svc.bootstrap(
|
|
99
|
+
task_hint=req.task_hint,
|
|
100
|
+
max_tokens=req.max_tokens,
|
|
101
|
+
indexing_depth=req.indexing_depth,
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router_v1.get("/hydration")
|
|
106
|
+
async def hydration_status(
|
|
107
|
+
project_id: str,
|
|
108
|
+
branch: BranchParam = "",
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""Progressive AST / embedding hydration state (MCP ``hydration_status`` parity)."""
|
|
111
|
+
ensure_branch_supported(branch)
|
|
112
|
+
svc = await get_service_or_404(project_id)
|
|
113
|
+
return svc.hydration_status()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@router_v1.get("/symbols", response_model=TextResult)
|
|
117
|
+
async def symbols_v1(
|
|
118
|
+
project_id: str,
|
|
119
|
+
path: str = Query(...),
|
|
120
|
+
branch: BranchParam = "",
|
|
121
|
+
) -> TextResult:
|
|
122
|
+
"""List symbols in a file."""
|
|
123
|
+
ensure_branch_supported(branch)
|
|
124
|
+
svc = await get_service_or_404(project_id)
|
|
125
|
+
return TextResult(result=svc.symbols(path))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router_v1.get("/search-symbols", response_model=TextResult)
|
|
129
|
+
async def search_symbols_v1(
|
|
130
|
+
project_id: str,
|
|
131
|
+
name: str = Query(...),
|
|
132
|
+
branch: BranchParam = "",
|
|
133
|
+
) -> TextResult:
|
|
134
|
+
"""Fuzzy symbol search across project."""
|
|
135
|
+
ensure_branch_supported(branch)
|
|
136
|
+
svc = await get_service_or_404(project_id)
|
|
137
|
+
return TextResult(result=svc.search_symbols(name))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router_v1.get("/dependencies", response_model=TextResult)
|
|
141
|
+
async def dependencies_v1(
|
|
142
|
+
project_id: str,
|
|
143
|
+
path: str = Query(...),
|
|
144
|
+
branch: BranchParam = "",
|
|
145
|
+
) -> TextResult:
|
|
146
|
+
"""File dependencies (forward/reverse)."""
|
|
147
|
+
ensure_branch_supported(branch)
|
|
148
|
+
svc = await get_service_or_404(project_id)
|
|
149
|
+
return TextResult(result=svc.dependencies(path))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router_v1.get("/impact", response_model=TextResult)
|
|
153
|
+
async def impact_analysis_v1(
|
|
154
|
+
project_id: str,
|
|
155
|
+
files: Annotated[list[str], Query(description="Changed file paths")],
|
|
156
|
+
branch: BranchParam = "",
|
|
157
|
+
) -> TextResult:
|
|
158
|
+
"""Transitive impact analysis."""
|
|
159
|
+
ensure_branch_supported(branch)
|
|
160
|
+
svc = await get_service_or_404(project_id)
|
|
161
|
+
return TextResult(result=svc.impact_analysis(files))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router_v1.get("/cross-refs", response_model=TextResult)
|
|
165
|
+
async def cross_references_v1(
|
|
166
|
+
project_id: str,
|
|
167
|
+
symbol: str = Query(...),
|
|
168
|
+
branch: BranchParam = "",
|
|
169
|
+
) -> TextResult:
|
|
170
|
+
"""Symbol cross-references."""
|
|
171
|
+
ensure_branch_supported(branch)
|
|
172
|
+
svc = await get_service_or_404(project_id)
|
|
173
|
+
return TextResult(result=svc.cross_references(symbol))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@router_v1.get("/file-analysis", response_model=TextResult)
|
|
177
|
+
async def file_analysis_v1(
|
|
178
|
+
project_id: str,
|
|
179
|
+
path: str = Query(...),
|
|
180
|
+
branch: BranchParam = "",
|
|
181
|
+
) -> TextResult:
|
|
182
|
+
"""Detailed single-file analysis."""
|
|
183
|
+
ensure_branch_supported(branch)
|
|
184
|
+
svc = await get_service_or_404(project_id)
|
|
185
|
+
return TextResult(result=svc.file_analysis(path))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@router_v1.post("/dependency-graph", response_model=TextResult)
|
|
189
|
+
async def dependency_graph_v1(
|
|
190
|
+
project_id: str,
|
|
191
|
+
req: DependencyGraphRequest,
|
|
192
|
+
branch: BranchParam = "",
|
|
193
|
+
) -> TextResult:
|
|
194
|
+
"""Dependency graph from a starting file."""
|
|
195
|
+
ensure_branch_supported(branch)
|
|
196
|
+
svc = await get_service_or_404(project_id)
|
|
197
|
+
return TextResult(result=svc.dependency_graph(req.start_file, depth=req.depth))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@router_v1.get("/hotspots", response_model=TextResult)
|
|
201
|
+
async def hotspots_v1(
|
|
202
|
+
project_id: str,
|
|
203
|
+
branch: BranchParam = "",
|
|
204
|
+
top_n: int = 15,
|
|
205
|
+
) -> TextResult:
|
|
206
|
+
"""Risk/complexity analysis."""
|
|
207
|
+
ensure_branch_supported(branch)
|
|
208
|
+
svc = await get_service_or_404(project_id)
|
|
209
|
+
return TextResult(result=svc.hotspots(top_n=top_n))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router_v1.get("/conventions", response_model=TextResult)
|
|
213
|
+
async def conventions_v1(
|
|
214
|
+
project_id: str,
|
|
215
|
+
branch: BranchParam = "",
|
|
216
|
+
sample_size: int = 50,
|
|
217
|
+
path: str = "",
|
|
218
|
+
) -> TextResult:
|
|
219
|
+
"""Coding conventions."""
|
|
220
|
+
ensure_branch_supported(branch)
|
|
221
|
+
svc = await get_service_or_404(project_id)
|
|
222
|
+
return TextResult(result=svc.conventions(sample_size=sample_size, path=path))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router_v1.post("/explore", response_model=TextResult)
|
|
226
|
+
async def explore(
|
|
227
|
+
project_id: str,
|
|
228
|
+
req: ExploreRequest,
|
|
229
|
+
branch: BranchParam = "",
|
|
230
|
+
) -> TextResult:
|
|
231
|
+
"""Hierarchical drill-down navigation."""
|
|
232
|
+
ensure_branch_supported(branch)
|
|
233
|
+
svc = await get_service_or_404(project_id)
|
|
234
|
+
return TextResult(result=svc.explore_codebase(
|
|
235
|
+
path=req.path, max_items=req.max_items, importance_threshold=req.importance_threshold,
|
|
236
|
+
))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@router_v1.post("/security-scan", response_model=TextResult)
|
|
240
|
+
async def security_scan_v1(
|
|
241
|
+
project_id: str,
|
|
242
|
+
req: SecurityScanRequest,
|
|
243
|
+
branch: BranchParam = "",
|
|
244
|
+
) -> TextResult:
|
|
245
|
+
"""Security analysis."""
|
|
246
|
+
ensure_branch_supported(branch)
|
|
247
|
+
svc = await get_service_or_404(project_id)
|
|
248
|
+
return TextResult(result=svc.security_scan(mode=req.mode, path=req.path))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@router_v1.post("/repo-map-ranked", response_model=TextResult)
|
|
252
|
+
async def repo_map_ranked_v1(
|
|
253
|
+
project_id: str,
|
|
254
|
+
req: RepoMapRankedRequest,
|
|
255
|
+
branch: BranchParam = "",
|
|
256
|
+
) -> TextResult:
|
|
257
|
+
"""Task-aware repository ranking."""
|
|
258
|
+
ensure_branch_supported(branch)
|
|
259
|
+
svc = await get_service_or_404(project_id)
|
|
260
|
+
return TextResult(result=svc.repo_map_ranked(
|
|
261
|
+
task_context=req.task_context,
|
|
262
|
+
token_budget=req.token_budget,
|
|
263
|
+
exclude_tests=req.exclude_tests,
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router_v1.post("/dead-code", response_model=TextResult)
|
|
268
|
+
async def dead_code_v1(
|
|
269
|
+
project_id: str,
|
|
270
|
+
req: DeadCodeRequest,
|
|
271
|
+
branch: BranchParam = "",
|
|
272
|
+
) -> TextResult:
|
|
273
|
+
"""Dead-code detection."""
|
|
274
|
+
ensure_branch_supported(branch)
|
|
275
|
+
svc = await get_service_or_404(project_id)
|
|
276
|
+
return TextResult(result=svc.dead_code(
|
|
277
|
+
scope=req.scope,
|
|
278
|
+
entry_points=req.entry_points,
|
|
279
|
+
level=req.level,
|
|
280
|
+
min_confidence=req.min_confidence,
|
|
281
|
+
top_n=req.top_n,
|
|
282
|
+
))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@router_v1.post("/distill", response_model=TextResult)
|
|
286
|
+
async def distill_v1(
|
|
287
|
+
project_id: str,
|
|
288
|
+
req: DistillRequest,
|
|
289
|
+
branch: BranchParam = "",
|
|
290
|
+
) -> TextResult:
|
|
291
|
+
"""Compressed codebase context."""
|
|
292
|
+
ensure_branch_supported(branch)
|
|
293
|
+
svc = await get_service_or_404(project_id)
|
|
294
|
+
return TextResult(result=svc.distill(
|
|
295
|
+
files=req.files,
|
|
296
|
+
depth=req.depth,
|
|
297
|
+
level=req.level,
|
|
298
|
+
max_tokens=req.max_tokens,
|
|
299
|
+
))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@router_v1.post("/readiness-report", response_model=TextResult)
|
|
303
|
+
async def readiness_report_v1(
|
|
304
|
+
project_id: str,
|
|
305
|
+
req: ReadinessReportRequest,
|
|
306
|
+
branch: BranchParam = "",
|
|
307
|
+
) -> TextResult:
|
|
308
|
+
"""Readiness audit report."""
|
|
309
|
+
ensure_branch_supported(branch)
|
|
310
|
+
svc = await get_service_or_404(project_id)
|
|
311
|
+
return TextResult(result=svc.readiness_report(
|
|
312
|
+
phases=req.phases,
|
|
313
|
+
scope=req.scope,
|
|
314
|
+
tracer_bullets=req.tracer_bullets,
|
|
315
|
+
min_severity=req.min_severity,
|
|
316
|
+
))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router_v1.post("/bug-scan", response_model=TextResult)
|
|
320
|
+
async def bug_scan_v1(
|
|
321
|
+
project_id: str,
|
|
322
|
+
req: BugScanRequest,
|
|
323
|
+
branch: BranchParam = "",
|
|
324
|
+
) -> TextResult:
|
|
325
|
+
"""Scan the current diff for likely bugs."""
|
|
326
|
+
ensure_branch_supported(branch)
|
|
327
|
+
svc = await get_service_or_404(project_id)
|
|
328
|
+
return TextResult(result=svc.bug_scan(
|
|
329
|
+
base_branch=req.base_branch,
|
|
330
|
+
min_confidence=req.min_confidence,
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@router_v1.post("/notify", response_model=TextResult)
|
|
335
|
+
async def notify_files(project_id: str, req: NotifyFilesRequest) -> TextResult:
|
|
336
|
+
"""Notify about changed files."""
|
|
337
|
+
svc = await get_service_or_404(project_id)
|
|
338
|
+
return TextResult(result=svc.notify_file_changed(req.files))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ===================================================================
|
|
342
|
+
# v2 endpoints — structured JSON, use providers
|
|
343
|
+
# ===================================================================
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@router_v2.get("/symbols", response_model=SymbolListResponse)
|
|
347
|
+
async def symbols_v2(
|
|
348
|
+
project_id: str,
|
|
349
|
+
path: str = Query(""),
|
|
350
|
+
branch: BranchParam = "",
|
|
351
|
+
) -> SymbolListResponse:
|
|
352
|
+
"""List symbols in a file (structured)."""
|
|
353
|
+
provider = await get_analysis_provider(project_id)
|
|
354
|
+
return await provider.symbols(path, branch)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@router_v2.get("/search-symbols", response_model=SymbolSearchResponse)
|
|
358
|
+
async def search_symbols_v2(
|
|
359
|
+
project_id: str,
|
|
360
|
+
name: str = Query(...),
|
|
361
|
+
directory: str = Query("", alias="dir", description="Directory prefix filter"),
|
|
362
|
+
branch: BranchParam = "",
|
|
363
|
+
) -> SymbolSearchResponse:
|
|
364
|
+
"""Fuzzy symbol search (structured)."""
|
|
365
|
+
provider = await get_analysis_provider(project_id)
|
|
366
|
+
return await provider.search_symbols(name, branch, directory=directory)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@router_v2.get("/dependencies", response_model=DependencyResponse)
|
|
370
|
+
async def dependencies_v2(
|
|
371
|
+
project_id: str,
|
|
372
|
+
path: str = Query(...),
|
|
373
|
+
branch: BranchParam = "",
|
|
374
|
+
) -> DependencyResponse:
|
|
375
|
+
"""File dependencies (structured)."""
|
|
376
|
+
provider = await get_analysis_provider(project_id)
|
|
377
|
+
return await provider.dependencies(path, branch)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@router_v2.post("/dependency-graph", response_model=DependencyGraphResponse)
|
|
381
|
+
async def dependency_graph_v2(
|
|
382
|
+
project_id: str,
|
|
383
|
+
req: DependencyGraphRequest,
|
|
384
|
+
branch: BranchParam = "",
|
|
385
|
+
) -> DependencyGraphResponse:
|
|
386
|
+
"""Dependency graph (structured)."""
|
|
387
|
+
provider = await get_analysis_provider(project_id)
|
|
388
|
+
return await provider.dependency_graph(req.start_file, req.depth, branch, directory=req.directory)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@router_v2.get("/impact", response_model=ImpactAnalysisResponse)
|
|
392
|
+
async def impact_analysis_v2(
|
|
393
|
+
project_id: str,
|
|
394
|
+
files: Annotated[list[str], Query(description="Changed file paths")],
|
|
395
|
+
branch: BranchParam = "",
|
|
396
|
+
) -> ImpactAnalysisResponse:
|
|
397
|
+
"""Transitive impact analysis (structured)."""
|
|
398
|
+
provider = await get_analysis_provider(project_id)
|
|
399
|
+
return await provider.impact_analysis(files, branch)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@router_v2.get("/cross-refs", response_model=CrossRefResponse)
|
|
403
|
+
async def cross_references_v2(
|
|
404
|
+
project_id: str,
|
|
405
|
+
symbol: str = Query(...),
|
|
406
|
+
branch: BranchParam = "",
|
|
407
|
+
) -> CrossRefResponse:
|
|
408
|
+
"""Symbol cross-references (structured)."""
|
|
409
|
+
provider = await get_analysis_provider(project_id)
|
|
410
|
+
return await provider.cross_references(symbol, branch)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@router_v2.get("/file-analysis", response_model=FileAnalysisResponse)
|
|
414
|
+
async def file_analysis_v2(
|
|
415
|
+
project_id: str,
|
|
416
|
+
path: str = Query(...),
|
|
417
|
+
branch: BranchParam = "",
|
|
418
|
+
) -> FileAnalysisResponse:
|
|
419
|
+
"""Detailed single-file analysis (structured)."""
|
|
420
|
+
provider = await get_analysis_provider(project_id)
|
|
421
|
+
return await provider.file_analysis(path, branch)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@router_v2.get("/hotspots", response_model=HotspotsResponse)
|
|
425
|
+
async def hotspots_v2(
|
|
426
|
+
project_id: str,
|
|
427
|
+
branch: BranchParam = "",
|
|
428
|
+
top_n: int = Query(15, ge=1, le=200),
|
|
429
|
+
) -> HotspotsResponse:
|
|
430
|
+
"""Risk/complexity hotspots (structured)."""
|
|
431
|
+
provider = await get_analysis_provider(project_id)
|
|
432
|
+
return await provider.hotspots(branch, top_n)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@router_v2.get("/conventions", response_model=ConventionsResponse)
|
|
436
|
+
async def conventions_v2(
|
|
437
|
+
project_id: str,
|
|
438
|
+
branch: BranchParam = "",
|
|
439
|
+
sample_size: int = 50,
|
|
440
|
+
path: str = "",
|
|
441
|
+
) -> ConventionsResponse:
|
|
442
|
+
"""Coding conventions (structured)."""
|
|
443
|
+
provider = await get_analysis_provider(project_id)
|
|
444
|
+
return await provider.conventions(branch, sample_size, path)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
# Rule-based analysis endpoints
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@router_v1.post("/analyze", response_model=TextResult)
|
|
453
|
+
async def analyze_v1(
|
|
454
|
+
project_id: str,
|
|
455
|
+
req: AnalyzeRequest,
|
|
456
|
+
branch: BranchParam = "",
|
|
457
|
+
) -> TextResult:
|
|
458
|
+
"""Run rule-based analysis with language packs."""
|
|
459
|
+
ensure_branch_supported(branch)
|
|
460
|
+
svc = await get_service_or_404(project_id)
|
|
461
|
+
return TextResult(result=svc.analyze(
|
|
462
|
+
files=req.files, path=req.path, language=req.language,
|
|
463
|
+
category=req.category, severity=req.severity, pack=req.pack,
|
|
464
|
+
min_confidence=req.min_confidence, max_findings=req.max_findings,
|
|
465
|
+
))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@router_v1.post("/list-rules", response_model=TextResult)
|
|
469
|
+
async def list_rules_v1(
|
|
470
|
+
project_id: str,
|
|
471
|
+
req: ListRulesRequest,
|
|
472
|
+
branch: BranchParam = "",
|
|
473
|
+
) -> TextResult:
|
|
474
|
+
"""List available analysis rules."""
|
|
475
|
+
ensure_branch_supported(branch)
|
|
476
|
+
svc = await get_service_or_404(project_id)
|
|
477
|
+
return TextResult(result=svc.list_rules(
|
|
478
|
+
language=req.language, category=req.category,
|
|
479
|
+
severity=req.severity, pack=req.pack, verbose=req.verbose,
|
|
480
|
+
))
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@router_v1.get("/list-packs", response_model=TextResult)
|
|
484
|
+
async def list_packs_v1(
|
|
485
|
+
project_id: str,
|
|
486
|
+
branch: BranchParam = "",
|
|
487
|
+
) -> TextResult:
|
|
488
|
+
"""List installed language analysis packs."""
|
|
489
|
+
ensure_branch_supported(branch)
|
|
490
|
+
svc = await get_service_or_404(project_id)
|
|
491
|
+
return TextResult(result=svc.list_packs())
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@router_v1.post("/install-pack", response_model=TextResult)
|
|
495
|
+
async def install_pack_v1(
|
|
496
|
+
project_id: str,
|
|
497
|
+
name: str,
|
|
498
|
+
branch: BranchParam = "",
|
|
499
|
+
) -> TextResult:
|
|
500
|
+
"""Install an example language pack into the project."""
|
|
501
|
+
ensure_branch_supported(branch)
|
|
502
|
+
svc = await get_service_or_404(project_id)
|
|
503
|
+
return TextResult(result=svc.install_pack(name=name))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@router_v1.post("/register-rule", response_model=TextResult)
|
|
507
|
+
async def register_rule_v1(
|
|
508
|
+
project_id: str,
|
|
509
|
+
req: RegisterRuleRequest,
|
|
510
|
+
branch: BranchParam = "",
|
|
511
|
+
) -> TextResult:
|
|
512
|
+
"""Register a custom YAML rule at runtime."""
|
|
513
|
+
ensure_branch_supported(branch)
|
|
514
|
+
svc = await get_service_or_404(project_id)
|
|
515
|
+
return TextResult(result=svc.register_rule(yaml_content=req.yaml_content))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""API key management endpoints (service mode only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from sqlalchemy import func, select
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from attocode_intel.api.auth import resolve_auth
|
|
13
|
+
from attocode_intel.api.auth.api_keys import generate_api_key
|
|
14
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
15
|
+
from attocode_intel.api.deps import get_db_session
|
|
16
|
+
from attocode_intel.db.models import ApiKey, OrgMembership
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/api/v1/orgs/{org_id}/api-keys", tags=["api-keys"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CreateKeyRequest(BaseModel):
|
|
22
|
+
name: str
|
|
23
|
+
scopes: list[str] = []
|
|
24
|
+
expires_in_days: int | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KeyResponse(BaseModel):
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
key_prefix: str
|
|
31
|
+
scopes: list[str]
|
|
32
|
+
created_at: str
|
|
33
|
+
expires_at: str | None = None
|
|
34
|
+
plaintext_key: str | None = None # Only set on creation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class KeyListResponse(BaseModel):
|
|
38
|
+
keys: list[KeyResponse]
|
|
39
|
+
total: int = 0
|
|
40
|
+
limit: int = 20
|
|
41
|
+
offset: int = 0
|
|
42
|
+
has_more: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _require_org_admin(
|
|
46
|
+
org_id: uuid.UUID, auth: AuthContext, session: AsyncSession
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Verify user is admin+ in the org."""
|
|
49
|
+
if not auth.user_id:
|
|
50
|
+
raise HTTPException(status_code=403, detail="Authentication required")
|
|
51
|
+
result = await session.execute(
|
|
52
|
+
select(OrgMembership).where(
|
|
53
|
+
OrgMembership.org_id == org_id,
|
|
54
|
+
OrgMembership.user_id == auth.user_id,
|
|
55
|
+
OrgMembership.role.in_(["owner", "admin"]),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if result.scalar_one_or_none() is None:
|
|
59
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("", response_model=KeyResponse)
|
|
63
|
+
async def create_api_key(
|
|
64
|
+
org_id: uuid.UUID,
|
|
65
|
+
req: CreateKeyRequest,
|
|
66
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
67
|
+
session: AsyncSession = Depends(get_db_session),
|
|
68
|
+
) -> KeyResponse:
|
|
69
|
+
"""Create a new API key. The plaintext key is shown only once."""
|
|
70
|
+
await _require_org_admin(org_id, auth, session)
|
|
71
|
+
|
|
72
|
+
plaintext, key_hash, key_prefix = generate_api_key()
|
|
73
|
+
|
|
74
|
+
expires_at = None
|
|
75
|
+
if req.expires_in_days:
|
|
76
|
+
from datetime import datetime, timedelta, timezone
|
|
77
|
+
|
|
78
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=req.expires_in_days)
|
|
79
|
+
|
|
80
|
+
api_key = ApiKey(
|
|
81
|
+
org_id=org_id,
|
|
82
|
+
user_id=auth.user_id,
|
|
83
|
+
name=req.name,
|
|
84
|
+
key_hash=key_hash,
|
|
85
|
+
key_prefix=key_prefix,
|
|
86
|
+
scopes=req.scopes,
|
|
87
|
+
expires_at=expires_at,
|
|
88
|
+
)
|
|
89
|
+
session.add(api_key)
|
|
90
|
+
await session.commit()
|
|
91
|
+
await session.refresh(api_key)
|
|
92
|
+
|
|
93
|
+
from attocode_intel.audit import log_event
|
|
94
|
+
|
|
95
|
+
await log_event(session, org_id, "api_key.created", user_id=auth.user_id, detail={"key_name": req.name, "key_prefix": key_prefix})
|
|
96
|
+
|
|
97
|
+
return KeyResponse(
|
|
98
|
+
id=str(api_key.id),
|
|
99
|
+
name=api_key.name,
|
|
100
|
+
key_prefix=api_key.key_prefix,
|
|
101
|
+
scopes=list(api_key.scopes),
|
|
102
|
+
created_at=api_key.created_at.isoformat(),
|
|
103
|
+
expires_at=api_key.expires_at.isoformat() if api_key.expires_at else None,
|
|
104
|
+
plaintext_key=plaintext,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.get("", response_model=KeyListResponse)
|
|
109
|
+
async def list_api_keys(
|
|
110
|
+
org_id: uuid.UUID,
|
|
111
|
+
limit: int = Query(20, ge=1, le=100),
|
|
112
|
+
offset: int = Query(0, ge=0),
|
|
113
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
114
|
+
session: AsyncSession = Depends(get_db_session),
|
|
115
|
+
) -> KeyListResponse:
|
|
116
|
+
"""List API keys (prefix + metadata only, no secrets)."""
|
|
117
|
+
await _require_org_admin(org_id, auth, session)
|
|
118
|
+
|
|
119
|
+
count_result = await session.execute(
|
|
120
|
+
select(func.count()).select_from(
|
|
121
|
+
select(ApiKey).where(ApiKey.org_id == org_id).subquery()
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
total = count_result.scalar() or 0
|
|
125
|
+
|
|
126
|
+
result = await session.execute(
|
|
127
|
+
select(ApiKey).where(ApiKey.org_id == org_id)
|
|
128
|
+
.offset(offset).limit(limit)
|
|
129
|
+
)
|
|
130
|
+
keys = []
|
|
131
|
+
for k in result.scalars():
|
|
132
|
+
keys.append(KeyResponse(
|
|
133
|
+
id=str(k.id),
|
|
134
|
+
name=k.name,
|
|
135
|
+
key_prefix=k.key_prefix,
|
|
136
|
+
scopes=list(k.scopes),
|
|
137
|
+
created_at=k.created_at.isoformat(),
|
|
138
|
+
expires_at=k.expires_at.isoformat() if k.expires_at else None,
|
|
139
|
+
))
|
|
140
|
+
return KeyListResponse(
|
|
141
|
+
keys=keys, total=total, limit=limit, offset=offset,
|
|
142
|
+
has_more=(offset + limit < total),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.delete("/{key_id}")
|
|
147
|
+
async def revoke_api_key(
|
|
148
|
+
org_id: uuid.UUID,
|
|
149
|
+
key_id: uuid.UUID,
|
|
150
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
151
|
+
session: AsyncSession = Depends(get_db_session),
|
|
152
|
+
) -> dict:
|
|
153
|
+
"""Revoke (delete) an API key."""
|
|
154
|
+
await _require_org_admin(org_id, auth, session)
|
|
155
|
+
|
|
156
|
+
result = await session.execute(
|
|
157
|
+
select(ApiKey).where(ApiKey.id == key_id, ApiKey.org_id == org_id)
|
|
158
|
+
)
|
|
159
|
+
api_key = result.scalar_one_or_none()
|
|
160
|
+
if api_key is None:
|
|
161
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
|
162
|
+
|
|
163
|
+
await session.delete(api_key)
|
|
164
|
+
|
|
165
|
+
from attocode_intel.audit import log_event
|
|
166
|
+
|
|
167
|
+
await log_event(session, org_id, "api_key.revoked", user_id=auth.user_id, detail={"key_id": str(key_id), "key_name": api_key.name})
|
|
168
|
+
|
|
169
|
+
await session.commit()
|
|
170
|
+
return {"detail": "API key revoked"}
|