sourcerykit 1.0.0b1__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.
- sourcerykit/__init__.py +31 -0
- sourcerykit/bootstrap/__init__.py +3 -0
- sourcerykit/bootstrap/_cache.py +118 -0
- sourcerykit/bootstrap/bootstrap.py +46 -0
- sourcerykit/cli/__init__.py +0 -0
- sourcerykit/cli/config.py +136 -0
- sourcerykit/cli/doctor.py +184 -0
- sourcerykit/cli/endpoints.py +56 -0
- sourcerykit/cli/feedback.py +57 -0
- sourcerykit/cli/init.py +360 -0
- sourcerykit/cli/logo.py +29 -0
- sourcerykit/cli/main.py +50 -0
- sourcerykit/cli/trace.py +237 -0
- sourcerykit/cli/utils.py +160 -0
- sourcerykit/config.py +213 -0
- sourcerykit/db/__init__.py +1 -0
- sourcerykit/db/_engine.py +98 -0
- sourcerykit/db/_intercepts.py +75 -0
- sourcerykit/db/_schema.py +139 -0
- sourcerykit/db/_traces.py +140 -0
- sourcerykit/db/_trusted_endpoints.py +107 -0
- sourcerykit/errors/__init__.py +32 -0
- sourcerykit/evaluator/__init__.py +3 -0
- sourcerykit/evaluator/_eval_modes.py +151 -0
- sourcerykit/evaluator/evaluator.py +120 -0
- sourcerykit/handoff/__init__.py +5 -0
- sourcerykit/handoff/_guide.py +46 -0
- sourcerykit/handoff/_preprocess.py +21 -0
- sourcerykit/handoff/_query_records.py +43 -0
- sourcerykit/handoff/payload_builder.py +185 -0
- sourcerykit/intercept/__init__.py +8 -0
- sourcerykit/intercept/_aiohttp_hook.py +62 -0
- sourcerykit/intercept/_httpx_hook.py +86 -0
- sourcerykit/intercept/_loader.py +44 -0
- sourcerykit/intercept/_self_egress.py +26 -0
- sourcerykit/intercept/_storage.py +59 -0
- sourcerykit/intercept/interceptor.py +85 -0
- sourcerykit/intercept/requests_hook.py +97 -0
- sourcerykit/logger.py +24 -0
- sourcerykit/provably/__init__.py +3 -0
- sourcerykit/provably/_answer_model.py +79 -0
- sourcerykit/provably/_api.py +369 -0
- sourcerykit/provably/_auth_api.py +157 -0
- sourcerykit/provably/_errors.py +150 -0
- sourcerykit/provably/_http.py +170 -0
- sourcerykit/provably/auth_service.py +122 -0
- sourcerykit/provably/service.py +617 -0
- sourcerykit/schemas/__init__.py +6 -0
- sourcerykit/schemas/agent_response.py +28 -0
- sourcerykit/schemas/handoff.py +109 -0
- sourcerykit/schemas/outcome.py +9 -0
- sourcerykit/schemas/verification_mode.py +8 -0
- sourcerykit/trusted_endpoints/__init__.py +11 -0
- sourcerykit/trusted_endpoints/service.py +187 -0
- sourcerykit/utils/__init__.py +5 -0
- sourcerykit/utils/validation.py +20 -0
- sourcerykit-1.0.0b1.dist-info/METADATA +228 -0
- sourcerykit-1.0.0b1.dist-info/RECORD +61 -0
- sourcerykit-1.0.0b1.dist-info/WHEEL +4 -0
- sourcerykit-1.0.0b1.dist-info/entry_points.txt +2 -0
- sourcerykit-1.0.0b1.dist-info/licenses/LICENSE.md +101 -0
sourcerykit/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from sourcerykit.bootstrap import bootstrap_system
|
|
2
|
+
from sourcerykit.errors import (
|
|
3
|
+
SourceryKitBootstrapError,
|
|
4
|
+
SourceryKitConfigError,
|
|
5
|
+
SourceryKitError,
|
|
6
|
+
SourceryKitStorageError,
|
|
7
|
+
SourceryKitTrustError,
|
|
8
|
+
)
|
|
9
|
+
from sourcerykit.evaluator import evaluate_handoff
|
|
10
|
+
from sourcerykit.handoff import build_handoff_payload
|
|
11
|
+
from sourcerykit.intercept import async_intercept_context, take_last_intercept_row_id
|
|
12
|
+
from sourcerykit.schemas import SourceryKitAgentResponse, VerificationMode
|
|
13
|
+
from sourcerykit.trusted_endpoints import insert_trusted_endpoint
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"bootstrap_system",
|
|
17
|
+
"evaluate_handoff",
|
|
18
|
+
"build_handoff_payload",
|
|
19
|
+
"async_intercept_context",
|
|
20
|
+
"take_last_intercept_row_id",
|
|
21
|
+
"insert_trusted_endpoint",
|
|
22
|
+
# Types
|
|
23
|
+
"VerificationMode",
|
|
24
|
+
"SourceryKitAgentResponse",
|
|
25
|
+
# Exceptions
|
|
26
|
+
"SourceryKitError",
|
|
27
|
+
"SourceryKitConfigError",
|
|
28
|
+
"SourceryKitBootstrapError",
|
|
29
|
+
"SourceryKitStorageError",
|
|
30
|
+
"SourceryKitTrustError",
|
|
31
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Provably bootstrap: middleware + database + collection + integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from sourcerykit.config import Settings
|
|
9
|
+
from sourcerykit.db._engine import ConnectionInfo, get_connection_info
|
|
10
|
+
from sourcerykit.errors import SourceryKitBootstrapError, SourceryKitError
|
|
11
|
+
from sourcerykit.logger import get_logger
|
|
12
|
+
from sourcerykit.provably._errors import ProvablyError
|
|
13
|
+
from sourcerykit.provably.service import service
|
|
14
|
+
|
|
15
|
+
_log = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ProvablyBootstrapCache:
|
|
20
|
+
"""Holds the resolved Provably resource IDs cached for the process lifetime."""
|
|
21
|
+
|
|
22
|
+
middleware_id: UUID | None = field(default=None)
|
|
23
|
+
database_id: UUID | None = field(default=None)
|
|
24
|
+
schema_id: UUID | None = field(default=None)
|
|
25
|
+
table_id: UUID | None = field(default=None)
|
|
26
|
+
collection_id: UUID | None = field(default=None)
|
|
27
|
+
integration_key: str | None = field(default=None)
|
|
28
|
+
collection_name: str | None = field(default=None)
|
|
29
|
+
|
|
30
|
+
def load_from(self, settings: Settings) -> None:
|
|
31
|
+
"""Populate cache from pre-resolved settings."""
|
|
32
|
+
self.middleware_id = settings.middleware_id
|
|
33
|
+
self.database_id = settings.database_id
|
|
34
|
+
self.schema_id = settings.schema_id
|
|
35
|
+
self.table_id = settings.table_id
|
|
36
|
+
self.collection_id = settings.collection_id
|
|
37
|
+
self.integration_key = settings.integration_key
|
|
38
|
+
self.collection_name = settings.project_name or None
|
|
39
|
+
|
|
40
|
+
async def run_handshake(self, project_name: str) -> None:
|
|
41
|
+
"""Resolve or create all required Provably resources."""
|
|
42
|
+
try:
|
|
43
|
+
self.middleware_id = await self._resolve_middleware()
|
|
44
|
+
|
|
45
|
+
connection_info = get_connection_info()
|
|
46
|
+
self.database_id = await self._resolve_database(connection_info)
|
|
47
|
+
|
|
48
|
+
ids = await service.get_database_schema_id_and_table_id(self.middleware_id, connection_info)
|
|
49
|
+
self.schema_id = ids["schema_id"]
|
|
50
|
+
self.table_id = ids["table_id"]
|
|
51
|
+
|
|
52
|
+
self.collection_name = project_name
|
|
53
|
+
self.collection_id = await self._resolve_collection(project_name)
|
|
54
|
+
self.integration_key = await self._resolve_integration_key()
|
|
55
|
+
except SourceryKitError:
|
|
56
|
+
raise
|
|
57
|
+
except Exception as e:
|
|
58
|
+
_log.error("handshake_failed_unexpected", error=str(e))
|
|
59
|
+
raise SourceryKitBootstrapError("Unexpected error during Provably handshake") from e
|
|
60
|
+
|
|
61
|
+
async def _resolve_middleware(self) -> UUID:
|
|
62
|
+
try:
|
|
63
|
+
return await service.get_middleware_id()
|
|
64
|
+
except ProvablyError:
|
|
65
|
+
_log.info("middleware_not_found_creating_new")
|
|
66
|
+
try:
|
|
67
|
+
return await service.create_middleware()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
_log.error("middleware_creation_failed", error=str(e))
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
async def _resolve_database(self, database: ConnectionInfo) -> UUID:
|
|
73
|
+
if self.middleware_id is None:
|
|
74
|
+
raise SourceryKitBootstrapError("middleware_id is not set; run_handshake() must be called first")
|
|
75
|
+
try:
|
|
76
|
+
return await service.get_database_id(self.middleware_id, database)
|
|
77
|
+
except ProvablyError:
|
|
78
|
+
_log.info("database_not_found_creating_new")
|
|
79
|
+
try:
|
|
80
|
+
return await service.create_database(self.middleware_id, database)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
_log.error("database_creation_failed", error=str(e))
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
async def _resolve_collection(self, project_name: str) -> UUID:
|
|
86
|
+
if self.middleware_id is None or self.database_id is None or self.schema_id is None or self.table_id is None:
|
|
87
|
+
raise SourceryKitBootstrapError(
|
|
88
|
+
"middleware_id, database_id, schema_id, and table_id must all be resolved before creating a collection"
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
return await service.get_collection_id(name=project_name)
|
|
92
|
+
except (ProvablyError, ValueError):
|
|
93
|
+
_log.info("collection_not_found_creating_new")
|
|
94
|
+
try:
|
|
95
|
+
columns = await service.get_columns_from_database(
|
|
96
|
+
self.middleware_id, self.database_id, self.schema_id, self.table_id
|
|
97
|
+
)
|
|
98
|
+
return await service.create_collection(
|
|
99
|
+
self.middleware_id,
|
|
100
|
+
self.database_id,
|
|
101
|
+
self.schema_id,
|
|
102
|
+
self.table_id,
|
|
103
|
+
columns,
|
|
104
|
+
name=project_name,
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
_log.error("collection_creation_failed", error=str(e))
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
async def _resolve_integration_key(self) -> str:
|
|
111
|
+
if self.collection_id is None:
|
|
112
|
+
raise SourceryKitBootstrapError("collection_id is not set; _resolve_collection() must succeed first")
|
|
113
|
+
_, key = await service.create_integration(self.collection_id)
|
|
114
|
+
return key
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Module-level singleton.
|
|
118
|
+
_BOOTSTRAP_INSTANCE = ProvablyBootstrapCache()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from sourcerykit.bootstrap._cache import _BOOTSTRAP_INSTANCE, ProvablyBootstrapCache
|
|
2
|
+
from sourcerykit.config import get_settings
|
|
3
|
+
from sourcerykit.db._engine import get_engine
|
|
4
|
+
from sourcerykit.db._schema import ensure_schema
|
|
5
|
+
from sourcerykit.errors import (
|
|
6
|
+
SourceryKitConfigError,
|
|
7
|
+
SourceryKitStorageError,
|
|
8
|
+
)
|
|
9
|
+
from sourcerykit.intercept.interceptor import init_interceptor
|
|
10
|
+
from sourcerykit.logger import get_logger
|
|
11
|
+
|
|
12
|
+
_log = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def bootstrap_system() -> None:
|
|
16
|
+
"""System entry point called exactly once during container/server startup."""
|
|
17
|
+
_log.info("system_bootstrap_started")
|
|
18
|
+
|
|
19
|
+
# Validate configuration
|
|
20
|
+
settings = get_settings()
|
|
21
|
+
|
|
22
|
+
if not settings.postgres_url:
|
|
23
|
+
raise SourceryKitConfigError("SOURCERYKIT_POSTGRES_URL is required. Run 'sourcerykit init' first.")
|
|
24
|
+
|
|
25
|
+
# Initialize database schemas
|
|
26
|
+
try:
|
|
27
|
+
await ensure_schema(get_engine())
|
|
28
|
+
except Exception as e:
|
|
29
|
+
_log.error("bootstrap_db_schema_failed", error=str(e))
|
|
30
|
+
raise SourceryKitStorageError("Failed to create database schema during bootstrap") from e
|
|
31
|
+
|
|
32
|
+
# Populate from cached settings or run handshake
|
|
33
|
+
if settings.has_bootstrap_ids:
|
|
34
|
+
_log.info("bootstrap_using_cached_ids")
|
|
35
|
+
_BOOTSTRAP_INSTANCE.load_from(settings)
|
|
36
|
+
else:
|
|
37
|
+
_log.info("bootstrap_running_handshake")
|
|
38
|
+
await _BOOTSTRAP_INSTANCE.run_handshake(project_name=settings.project_name)
|
|
39
|
+
|
|
40
|
+
init_interceptor()
|
|
41
|
+
_log.info("system_bootstrap_completed")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_bootstrap() -> ProvablyBootstrapCache:
|
|
45
|
+
"""Synchronous gateway to access resolved IDs."""
|
|
46
|
+
return _BOOTSTRAP_INSTANCE
|
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from sourcerykit.cli.init import clear_auth_caches, create_db_tables, run_provably_handshake, save_bootstrap_ids
|
|
7
|
+
from sourcerykit.cli.utils import (
|
|
8
|
+
console,
|
|
9
|
+
mask_postgres_url,
|
|
10
|
+
mask_secret,
|
|
11
|
+
prompt_postgres_url_with_retry,
|
|
12
|
+
prompt_project_name,
|
|
13
|
+
require_settings,
|
|
14
|
+
)
|
|
15
|
+
from sourcerykit.config import get_settings, load_local_env, save_app_dir_config, save_local_env
|
|
16
|
+
|
|
17
|
+
config = typer.Typer(no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@config.command()
|
|
21
|
+
def list(show_key: bool = typer.Option(False, "--show-key", help="show secrets in clear text")) -> None:
|
|
22
|
+
"""Pretty print the active configuration (global + local)."""
|
|
23
|
+
settings = require_settings()
|
|
24
|
+
|
|
25
|
+
api_key_display = settings.api_key if show_key else mask_secret(settings.api_key)
|
|
26
|
+
console.print("\nš [bold]Global Config[/bold] \n")
|
|
27
|
+
console.print(f"[cyan]PROVABLY_API_KEY[/cyan] = [yellow]'{api_key_display}'[/yellow]")
|
|
28
|
+
|
|
29
|
+
pg_display = settings.postgres_url if show_key else mask_postgres_url(settings.postgres_url)
|
|
30
|
+
|
|
31
|
+
console.print("\nš [bold]Local Config[/bold] (.env)\n")
|
|
32
|
+
console.print(f"[cyan]SOURCERYKIT_POSTGRES_URL[/cyan] = [yellow]'{pg_display}'[/yellow]")
|
|
33
|
+
console.print(f"[cyan]SOURCERYKIT_PROJECT_NAME[/cyan] = [yellow]'{settings.project_name}'[/yellow]")
|
|
34
|
+
|
|
35
|
+
console.print()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@config.command()
|
|
39
|
+
def set() -> None:
|
|
40
|
+
"""Interactively set or update configuration variables."""
|
|
41
|
+
console.print("\nāļø [bold]SourceryKit Configuration Setup[/bold]\n")
|
|
42
|
+
|
|
43
|
+
choices = questionary.checkbox(
|
|
44
|
+
"Which configuration variables would you like to update?",
|
|
45
|
+
choices=[
|
|
46
|
+
questionary.Choice("PROVABLY_API_KEY (global)", checked=False),
|
|
47
|
+
questionary.Choice("SOURCERYKIT_POSTGRES_URL (local)", checked=False),
|
|
48
|
+
questionary.Choice("SOURCERYKIT_PROJECT_NAME (local)", checked=False),
|
|
49
|
+
],
|
|
50
|
+
).ask()
|
|
51
|
+
|
|
52
|
+
if not choices:
|
|
53
|
+
console.print("[yellow]No variables selected. Configuration unchanged.[/yellow]")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# --- Collect inputs ---
|
|
57
|
+
|
|
58
|
+
api_key = None
|
|
59
|
+
if "PROVABLY_API_KEY (global)" in choices:
|
|
60
|
+
api_key = questionary.password("Enter your PROVABLY_API_KEY:").ask()
|
|
61
|
+
if api_key is not None:
|
|
62
|
+
api_key = api_key.strip()
|
|
63
|
+
if not api_key:
|
|
64
|
+
console.print("[red]ā API key cannot be empty.[/red]")
|
|
65
|
+
api_key = None
|
|
66
|
+
elif not re.fullmatch(r"zk-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", api_key):
|
|
67
|
+
console.print("[red]ā API key must match format zk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx[/red]")
|
|
68
|
+
api_key = None
|
|
69
|
+
|
|
70
|
+
postgres_url = None
|
|
71
|
+
postgres_changed = False
|
|
72
|
+
if "SOURCERYKIT_POSTGRES_URL (local)" in choices:
|
|
73
|
+
local_env = load_local_env()
|
|
74
|
+
current_url = local_env.get("SOURCERYKIT_POSTGRES_URL", "")
|
|
75
|
+
result = prompt_postgres_url_with_retry()
|
|
76
|
+
if result and result != current_url:
|
|
77
|
+
postgres_url = result
|
|
78
|
+
postgres_changed = True
|
|
79
|
+
elif result == current_url:
|
|
80
|
+
console.print("[yellow]Same URL ā no change.[/yellow]")
|
|
81
|
+
|
|
82
|
+
project_name = None
|
|
83
|
+
project_changed = False
|
|
84
|
+
if "SOURCERYKIT_PROJECT_NAME (local)" in choices:
|
|
85
|
+
local_env = load_local_env()
|
|
86
|
+
current_name = local_env.get("SOURCERYKIT_PROJECT_NAME", "")
|
|
87
|
+
result = prompt_project_name(current=current_name)
|
|
88
|
+
if result and result != current_name:
|
|
89
|
+
project_name = result
|
|
90
|
+
project_changed = True
|
|
91
|
+
elif result == current_name:
|
|
92
|
+
console.print("[yellow]Same name ā no change.[/yellow]")
|
|
93
|
+
|
|
94
|
+
# --- Save config ---
|
|
95
|
+
|
|
96
|
+
if api_key:
|
|
97
|
+
save_app_dir_config(api_key=api_key)
|
|
98
|
+
|
|
99
|
+
local_updates: dict[str, str] = {}
|
|
100
|
+
if postgres_url:
|
|
101
|
+
local_updates["SOURCERYKIT_POSTGRES_URL"] = postgres_url
|
|
102
|
+
if project_name:
|
|
103
|
+
local_updates["SOURCERYKIT_PROJECT_NAME"] = project_name
|
|
104
|
+
if local_updates:
|
|
105
|
+
save_local_env(**local_updates)
|
|
106
|
+
|
|
107
|
+
# --- Re-bootstrap if needed ---
|
|
108
|
+
|
|
109
|
+
if postgres_changed or project_changed:
|
|
110
|
+
settings = get_settings()
|
|
111
|
+
name = settings.project_name
|
|
112
|
+
|
|
113
|
+
clear_auth_caches()
|
|
114
|
+
|
|
115
|
+
if postgres_changed:
|
|
116
|
+
console.print("\nš§ [bold]Re-bootstrapping (DB + Provably)...[/bold]")
|
|
117
|
+
try:
|
|
118
|
+
create_db_tables()
|
|
119
|
+
console.print(" ā
Database tables created")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
console.print(f" [red]ā DB tables failed: {e}[/red]")
|
|
122
|
+
return
|
|
123
|
+
else:
|
|
124
|
+
console.print("\nš§ [bold]Re-running Provably handshake (new collection name)...[/bold]")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
run_provably_handshake(name)
|
|
128
|
+
console.print(" ā
Provably handshake completed")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
console.print(f" [red]ā Handshake failed: {e}[/red]")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
save_bootstrap_ids()
|
|
134
|
+
console.print(" ā
Bootstrap IDs saved\n")
|
|
135
|
+
|
|
136
|
+
console.print("[bold green]⨠Configuration updated.[/bold green]")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Doctor command ā validate configuration and connectivity."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from sourcerykit.cli.init import run_full_bootstrap
|
|
8
|
+
from sourcerykit.cli.utils import console, run_connectivity_check
|
|
9
|
+
from sourcerykit.config import Settings, get_settings
|
|
10
|
+
from sourcerykit.db._engine import get_connection_info
|
|
11
|
+
from sourcerykit.provably._errors import ProvablyConnectionError, ProvablyUnauthorizedError
|
|
12
|
+
from sourcerykit.provably.auth_service import auth_service
|
|
13
|
+
from sourcerykit.provably.service import service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _check_api_key_and_org(settings: Settings) -> tuple[bool, str]:
|
|
17
|
+
"""Validate API key and org_id in one call (list_organizations uses API key)."""
|
|
18
|
+
if not settings.api_key:
|
|
19
|
+
return False, "PROVABLY_API_KEY is missing ā run 'sourcerykit init'"
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
orgs = asyncio.run(auth_service.list_organizations())
|
|
23
|
+
except ProvablyUnauthorizedError:
|
|
24
|
+
return False, "API key is invalid or expired ā run 'sourcerykit init'"
|
|
25
|
+
except ProvablyConnectionError:
|
|
26
|
+
return False, "Cannot reach Provably API (network error)"
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return False, f"API key check failed: {e}"
|
|
29
|
+
|
|
30
|
+
org_ids = [str(o.get("id", "")) for o in orgs]
|
|
31
|
+
if str(settings.org_id) not in org_ids:
|
|
32
|
+
return False, f"Org ID {settings.org_id} not found ā run 'sourcerykit init'"
|
|
33
|
+
|
|
34
|
+
return True, f"API key valid, org found ({len(orgs)} org(s))"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_postgres(settings: Settings) -> tuple[bool, str]:
|
|
38
|
+
"""Validate postgres_url connectivity."""
|
|
39
|
+
if not settings.postgres_url:
|
|
40
|
+
return False, "SOURCERYKIT_POSTGRES_URL is missing ā run 'sourcerykit init'"
|
|
41
|
+
|
|
42
|
+
if run_connectivity_check(settings.postgres_url, quiet=True):
|
|
43
|
+
return True, "PostgreSQL connection successful"
|
|
44
|
+
return False, "PostgreSQL connection failed ā check your SOURCERYKIT_POSTGRES_URL"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_project_name(settings: Settings) -> tuple[bool, str]:
|
|
48
|
+
"""Validate project_name is set."""
|
|
49
|
+
if settings.project_name:
|
|
50
|
+
return True, f"'{settings.project_name}'"
|
|
51
|
+
return False, "SOURCERYKIT_PROJECT_NAME is missing ā run 'sourcerykit init'"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _check_bootstrap_ids(settings: Settings) -> tuple[bool, str]:
|
|
55
|
+
"""Validate all bootstrap resource IDs are present."""
|
|
56
|
+
if settings.has_bootstrap_ids:
|
|
57
|
+
return True, "All bootstrap IDs present"
|
|
58
|
+
|
|
59
|
+
missing = [
|
|
60
|
+
f.name
|
|
61
|
+
for f in dataclasses.fields(settings)
|
|
62
|
+
if (f.name.endswith("_id") or f.name == "integration_key") and not getattr(settings, f.name)
|
|
63
|
+
]
|
|
64
|
+
return False, f"Missing: {', '.join(missing)} ā run 'sourcerykit doctor --fix'"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --- Deep checks ā verify IDs exist in Provably backend ---
|
|
68
|
+
async def _deep_check_collection_and_ids(settings: Settings) -> tuple[bool, str]:
|
|
69
|
+
"""Verify Provably resources match local config using the same calls as bootstrap."""
|
|
70
|
+
if not settings.has_bootstrap_ids or settings.middleware_id is None:
|
|
71
|
+
return False, "Bootstrap IDs missing ā run 'sourcerykit doctor --fix'"
|
|
72
|
+
|
|
73
|
+
remote_mw = await service.get_middleware_id()
|
|
74
|
+
if remote_mw != settings.middleware_id:
|
|
75
|
+
return False, f"Middleware mismatch (local={settings.middleware_id}, remote={remote_mw})"
|
|
76
|
+
|
|
77
|
+
connection_info = get_connection_info()
|
|
78
|
+
remote_db = await service.get_database_id(settings.middleware_id, connection_info)
|
|
79
|
+
if remote_db != settings.database_id:
|
|
80
|
+
return False, f"Database mismatch (local={settings.database_id}, remote={remote_db})"
|
|
81
|
+
|
|
82
|
+
ids = await service.get_database_schema_id_and_table_id(settings.middleware_id, connection_info)
|
|
83
|
+
if ids["schema_id"] != settings.schema_id:
|
|
84
|
+
return False, f"Schema mismatch (local={settings.schema_id}, remote={ids['schema_id']})"
|
|
85
|
+
if ids["table_id"] != settings.table_id:
|
|
86
|
+
return False, f"Table mismatch (local={settings.table_id}, remote={ids['table_id']})"
|
|
87
|
+
|
|
88
|
+
remote_col = await service.get_collection_id(settings.project_name)
|
|
89
|
+
if remote_col != settings.collection_id:
|
|
90
|
+
return False, f"Collection mismatch (local={settings.collection_id}, remote={remote_col})"
|
|
91
|
+
|
|
92
|
+
return True, f"Collection '{settings.project_name}' verified (middleware, db, schema, table, collection)"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _deep_check_integration(settings: Settings) -> tuple[bool, str]:
|
|
96
|
+
"""Verify integration_key has the expected format (API key, not UUID)."""
|
|
97
|
+
if not settings.integration_key:
|
|
98
|
+
return False, "SOURCERYKIT_INTEGRATION_KEY is missing ā run 'sourcerykit doctor --fix'"
|
|
99
|
+
|
|
100
|
+
if not settings.integration_key.startswith("i-"):
|
|
101
|
+
return False, f"SOURCERYKIT_INTEGRATION_KEY has unexpected format: {settings.integration_key!r}"
|
|
102
|
+
|
|
103
|
+
return True, "Integration key format valid"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_deep_check_collection_and_ids(settings: Settings) -> tuple[bool, str]:
|
|
107
|
+
try:
|
|
108
|
+
return asyncio.run(_deep_check_collection_and_ids(settings))
|
|
109
|
+
except ProvablyUnauthorizedError:
|
|
110
|
+
return False, "API key expired ā run 'sourcerykit init'"
|
|
111
|
+
except ProvablyConnectionError:
|
|
112
|
+
return False, "Cannot reach Provably API (network error)"
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return False, f"Collection check failed: {e}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_deep_check_integration(settings: Settings) -> tuple[bool, str]:
|
|
118
|
+
try:
|
|
119
|
+
return asyncio.run(_deep_check_integration(settings))
|
|
120
|
+
except ProvablyUnauthorizedError:
|
|
121
|
+
return False, "API key expired ā run 'sourcerykit init'"
|
|
122
|
+
except ProvablyConnectionError:
|
|
123
|
+
return False, "Cannot reach Provably API (network error)"
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return False, f"Integration check failed: {e}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_doctor(fix: bool = False) -> None:
|
|
129
|
+
"""Validate global config, local config, and connectivity."""
|
|
130
|
+
console.print("\n𩺠[bold]SourceryKit Doctor[/bold]\n")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
settings = get_settings()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
console.print(f"[red]ā Cannot load settings: {e}[/red]")
|
|
136
|
+
console.print("\n[bold yellow]Run 'sourcerykit init' to configure[/bold yellow]\n")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
checks: list[tuple[str, Callable[[], tuple[bool, str]]]] = [
|
|
140
|
+
("API key + org", lambda: _check_api_key_and_org(settings)),
|
|
141
|
+
("PostgreSQL", lambda: _check_postgres(settings)),
|
|
142
|
+
("Project name", lambda: _check_project_name(settings)),
|
|
143
|
+
("Bootstrap IDs", lambda: _check_bootstrap_ids(settings)),
|
|
144
|
+
("Collection + IDs", lambda: _run_deep_check_collection_and_ids(settings)),
|
|
145
|
+
("Integration", lambda: _run_deep_check_integration(settings)),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
passed = 0
|
|
149
|
+
total = len(checks)
|
|
150
|
+
failed_indices: list[int] = []
|
|
151
|
+
|
|
152
|
+
for i, (label, check_fn) in enumerate(checks):
|
|
153
|
+
ok, detail = check_fn()
|
|
154
|
+
icon = "[green]ā
[/green]" if ok else "[red]ā[/red]"
|
|
155
|
+
console.print(f" {icon} {label}: {detail}")
|
|
156
|
+
if ok:
|
|
157
|
+
passed += 1
|
|
158
|
+
else:
|
|
159
|
+
failed_indices.append(i)
|
|
160
|
+
|
|
161
|
+
# --fix: attempt to create missing bootstrap IDs
|
|
162
|
+
if fix and failed_indices:
|
|
163
|
+
console.print("\n š§ [bold]Running bootstrap handshake...[/bold]", end=" ")
|
|
164
|
+
try:
|
|
165
|
+
if run_full_bootstrap(settings.project_name):
|
|
166
|
+
console.print("[green]DONE ā
[/green]")
|
|
167
|
+
get_settings.cache_clear()
|
|
168
|
+
settings = get_settings()
|
|
169
|
+
for i in failed_indices:
|
|
170
|
+
label, check_fn = checks[i]
|
|
171
|
+
ok, detail = check_fn()
|
|
172
|
+
icon = "[green]ā
[/green]" if ok else "[red]ā[/red]"
|
|
173
|
+
console.print(f" {icon} {label}: {detail}")
|
|
174
|
+
if ok:
|
|
175
|
+
passed += 1
|
|
176
|
+
else:
|
|
177
|
+
console.print("[red]FAILED ā[/red]")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
console.print(f"[red]FAILED ā[/red]\n {e}")
|
|
180
|
+
|
|
181
|
+
if passed == total:
|
|
182
|
+
console.print(f"\n[bold green]All {total} checks passed![/bold green]\n")
|
|
183
|
+
else:
|
|
184
|
+
console.print(f"\n[bold yellow]{passed}/{total} checks passed ā run 'sourcerykit init' to fix[/bold yellow]\n")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from sourcerykit.cli.utils import console, require_settings
|
|
8
|
+
from sourcerykit.errors import SourceryKitTrustError
|
|
9
|
+
from sourcerykit.trusted_endpoints import service
|
|
10
|
+
from sourcerykit.trusted_endpoints.service import sanitize_and_extract_trusted_url
|
|
11
|
+
|
|
12
|
+
endpoints = typer.Typer(no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@endpoints.command()
|
|
16
|
+
def add(url: str, label: str | None = typer.Option(None, "--label", "-l", help="optional display label")) -> None:
|
|
17
|
+
require_settings()
|
|
18
|
+
try:
|
|
19
|
+
sanitize_and_extract_trusted_url(url)
|
|
20
|
+
except SourceryKitTrustError as e:
|
|
21
|
+
console.print(f"[red]ā {e}[/red]")
|
|
22
|
+
raise typer.Exit(code=1)
|
|
23
|
+
asyncio.run(service.insert_trusted_endpoint(url=url, display_label=label))
|
|
24
|
+
console.print(f"[bold green]ā
Endpoint added:[/bold green] {url}" + (f" ({label})" if label else ""))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@endpoints.command()
|
|
28
|
+
def list() -> None:
|
|
29
|
+
require_settings()
|
|
30
|
+
rows = asyncio.run(service.list_all_trusted_endpoints_detailed())
|
|
31
|
+
|
|
32
|
+
if not rows:
|
|
33
|
+
console.print("[yellow]No trusted endpoints found.[/yellow]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
table = Table(title="Trusted Endpoints")
|
|
37
|
+
table.add_column("URL", style="cyan")
|
|
38
|
+
table.add_column("Label", style="white")
|
|
39
|
+
table.add_column("Policy", style="dim")
|
|
40
|
+
table.add_column("Created by", style="dim")
|
|
41
|
+
|
|
42
|
+
for ep in rows:
|
|
43
|
+
table.add_row(ep["url"], ep["label"], ep["policy_version"], ep["created_by"])
|
|
44
|
+
|
|
45
|
+
console.print(table)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@endpoints.command()
|
|
49
|
+
def remove(url: str) -> None:
|
|
50
|
+
require_settings()
|
|
51
|
+
confirm = questionary.confirm(f"Remove endpoint '{url}'?", default=False).ask()
|
|
52
|
+
if not confirm:
|
|
53
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
54
|
+
return
|
|
55
|
+
asyncio.run(service.remove_trusted_endpoint(url=url))
|
|
56
|
+
console.print(f"[bold green]ā
Endpoint removed:[/bold green] {url}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sourcerykit.cli.utils import console, require_settings
|
|
8
|
+
from sourcerykit.provably.service import ProvablyService
|
|
9
|
+
|
|
10
|
+
service = ProvablyService()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def send_feedback() -> None:
|
|
14
|
+
"""Submit interactive feedback or bug reports."""
|
|
15
|
+
require_settings()
|
|
16
|
+
console.print("\nš£ [bold]Send Feedback to SourceryKit[/bold]\n")
|
|
17
|
+
|
|
18
|
+
# read description
|
|
19
|
+
description = questionary.text("Please provide a description of your feedback/issue:", multiline=True).ask()
|
|
20
|
+
|
|
21
|
+
if not description or not description.strip():
|
|
22
|
+
console.print("[yellow]ā ļø Feedback description cannot be empty. Aborting.[/yellow]")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
|
|
25
|
+
# read file
|
|
26
|
+
attach_file = questionary.confirm(
|
|
27
|
+
"Would you like to attach a file (e.g., a log or screenshot)?", default=False
|
|
28
|
+
).ask()
|
|
29
|
+
|
|
30
|
+
file_bytes = b""
|
|
31
|
+
if attach_file:
|
|
32
|
+
file_path_str = questionary.text("Enter the path to the file you want to attach:").ask()
|
|
33
|
+
|
|
34
|
+
if file_path_str:
|
|
35
|
+
path = Path(file_path_str.strip()).expanduser().resolve()
|
|
36
|
+
|
|
37
|
+
if not path.exists() or not path.is_file():
|
|
38
|
+
console.print(f"[red]ā Error: File not found at '{path}'[/red]")
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Read the file directly into raw bytes
|
|
43
|
+
file_bytes = path.read_bytes()
|
|
44
|
+
console.print(f"[green]ā Attached '{path.name}' ({len(file_bytes)} bytes)[/green]")
|
|
45
|
+
except PermissionError:
|
|
46
|
+
console.print(f"[red]ā Permission denied when reading '{path}'[/red]")
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
|
|
49
|
+
console.print("\nš Sending feedback...")
|
|
50
|
+
|
|
51
|
+
# send feedback
|
|
52
|
+
try:
|
|
53
|
+
asyncio.run(service.create_feedback(description.strip(), file_bytes))
|
|
54
|
+
console.print("\n⨠[bold green]Success![/bold green] Thank you for your feedback.")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
console.print(f"\n[red]ā Failed to submit feedback: {e}[/red]")
|
|
57
|
+
raise typer.Exit(code=1)
|