buildai-cli 0.3.45__tar.gz → 0.3.47__tar.gz

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.
Files changed (32) hide show
  1. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/PKG-INFO +1 -1
  2. buildai_cli-0.3.47/cli/auth_proxy.py +99 -0
  3. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/migrate.py +13 -1
  4. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/context.py +27 -2
  5. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/guard.py +12 -4
  6. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/pyproject.toml +1 -1
  7. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/.gitignore +0 -0
  8. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/AGENTS.md +0 -0
  9. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/CLAUDE.md +0 -0
  10. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/buildai_bootstrap.py +0 -0
  11. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/__init__.py +0 -0
  12. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/_has_core.py +0 -0
  13. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/auth_local.py +0 -0
  14. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/__init__.py +0 -0
  15. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/api_proxy.py +0 -0
  16. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/auth.py +0 -0
  17. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/__init__.py +0 -0
  18. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/common.py +0 -0
  19. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/query.py +0 -0
  20. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/schema.py +0 -0
  21. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/db/status.py +0 -0
  22. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/dev.py +0 -0
  23. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/commands/doctor.py +0 -0
  24. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/config.py +0 -0
  25. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/console.py +0 -0
  26. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/internal_api.py +0 -0
  27. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/main.py +0 -0
  28. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/nl_query/__init__.py +0 -0
  29. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/nl_query/dataset_tools.py +0 -0
  30. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/ops_init.py +0 -0
  31. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/output.py +0 -0
  32. {buildai_cli-0.3.45 → buildai_cli-0.3.47}/cli/pagination.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.45
3
+ Version: 0.3.47
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,99 @@
1
+ """Helpers for the sanctioned local AlloyDB Auth Proxy lane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import socket
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+
12
+ from infra.settings import Settings
13
+
14
+
15
+ def _default_proxy_binary() -> str | None:
16
+ """Return the preferred AlloyDB Auth Proxy binary path if available."""
17
+
18
+ on_path = shutil.which("alloydb-auth-proxy")
19
+ if on_path:
20
+ return on_path
21
+ local_bin = Path.home() / ".local" / "bin" / "alloydb-auth-proxy"
22
+ if local_bin.is_file():
23
+ return str(local_bin)
24
+ return None
25
+
26
+
27
+ def proxy_is_listening(*, host: str, port: int) -> bool:
28
+ """Return True when a TCP listener is already present on the proxy endpoint."""
29
+
30
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
31
+ sock.settimeout(0.25)
32
+ return sock.connect_ex((host, port)) == 0
33
+
34
+
35
+ def ensure_alloydb_auth_proxy(
36
+ *,
37
+ settings: Settings,
38
+ target_service_account: str,
39
+ ) -> None:
40
+ """Start the sanctioned AlloyDB Auth Proxy if it is not already running.
41
+
42
+ The engineer DB lane should use one explicit transport path across CLI,
43
+ `psql`, and desktop tools. If the proxy is already listening, leave it
44
+ alone. Otherwise start it in the background and wait briefly for the port
45
+ to come up.
46
+ """
47
+
48
+ if not settings.effective_use_alloydb_auth_proxy:
49
+ return
50
+
51
+ host = settings.alloydb_auth_proxy_host
52
+ port = settings.effective_alloydb_auth_proxy_port
53
+ if proxy_is_listening(host=host, port=port):
54
+ return
55
+
56
+ proxy_binary = _default_proxy_binary()
57
+ if proxy_binary is None:
58
+ raise RuntimeError(
59
+ "alloydb-auth-proxy is not installed. Re-run ./scripts/setup.sh to provision "
60
+ "the canonical local DB transport."
61
+ )
62
+
63
+ if not settings.alloydb_instance_uri:
64
+ raise RuntimeError(
65
+ "AlloyDB instance URI is not configured, so the local Auth Proxy cannot start."
66
+ )
67
+
68
+ command = [
69
+ proxy_binary,
70
+ settings.alloydb_instance_uri,
71
+ "--address",
72
+ host,
73
+ "--port",
74
+ str(port),
75
+ "--auto-iam-authn",
76
+ f"--impersonate-service-account={target_service_account}",
77
+ "--disable-built-in-telemetry",
78
+ ]
79
+ if not settings.use_private_ip:
80
+ command.append("--public-ip")
81
+
82
+ subprocess.Popen(
83
+ command,
84
+ stdout=subprocess.DEVNULL,
85
+ stderr=subprocess.DEVNULL,
86
+ start_new_session=True,
87
+ env=os.environ.copy(),
88
+ )
89
+
90
+ deadline = time.monotonic() + 10.0
91
+ while time.monotonic() < deadline:
92
+ if proxy_is_listening(host=host, port=port):
93
+ return
94
+ time.sleep(0.25)
95
+
96
+ raise RuntimeError(
97
+ f"Timed out waiting for alloydb-auth-proxy on {host}:{port}. "
98
+ "Run `uv run buildai dev db info` to inspect the expected proxy recipe."
99
+ )
@@ -24,6 +24,14 @@ from .common import (
24
24
  set_owner_role,
25
25
  )
26
26
 
27
+ _MIGRATION_WRITE_PROFILES = frozenset(
28
+ {
29
+ "internal_admin",
30
+ "migrations-local",
31
+ "db-admin-local",
32
+ }
33
+ )
34
+
27
35
 
28
36
  def migrate(
29
37
  ctx: typer.Context,
@@ -41,7 +49,11 @@ def migrate(
41
49
  """Run the checked-in SQL migrations on the selected DB lane."""
42
50
 
43
51
  if target is not None and not dry_run:
44
- require_write(ctx, "Database migrations")
52
+ require_write(
53
+ ctx,
54
+ "Database migrations",
55
+ allowed_profiles=_MIGRATION_WRITE_PROFILES,
56
+ )
45
57
  settings: Settings = ctx.obj["settings"]
46
58
 
47
59
  async def run() -> None:
@@ -22,6 +22,7 @@ from dataclasses import dataclass
22
22
  from typing import TYPE_CHECKING, AsyncGenerator
23
23
 
24
24
  import asyncpg
25
+ from dal import scopes as dal_scopes
25
26
  from infra.settings import Settings, get_settings
26
27
 
27
28
  from infra import Database, get_logger
@@ -34,6 +35,22 @@ logger = get_logger(__name__)
34
35
  # Environment variable to force local PostgreSQL (for test/CI)
35
36
  USE_LOCAL_DB = os.getenv("USE_LOCAL_DB", "false").lower() == "true"
36
37
  _ZERO_UUID = "00000000-0000-0000-0000-000000000000"
38
+ _READ_SUFFIXES = (".read", ".search", ".query", ".introspection")
39
+ _READ_PROFILE_SCOPES = frozenset(
40
+ scope
41
+ for scope in dal_scopes.ALL_SCOPES
42
+ if any(scope.endswith(suffix) for suffix in _READ_SUFFIXES)
43
+ )
44
+ _CLI_PROFILE_SCOPES: dict[str, frozenset[str]] = {
45
+ "internal_admin": frozenset(dal_scopes.ALL_SCOPES),
46
+ "internal_viewer": _READ_PROFILE_SCOPES,
47
+ "engineers-dev": _READ_PROFILE_SCOPES,
48
+ "developer": frozenset(dal_scopes.ALL_SCOPES),
49
+ "operator": frozenset(dal_scopes.ALL_SCOPES),
50
+ "ml_engineer": frozenset(dal_scopes.ALL_SCOPES),
51
+ "mcp": _READ_PROFILE_SCOPES,
52
+ "agent": frozenset(dal_scopes.ALL_SCOPES),
53
+ }
37
54
  _UNRESTRICTED_CLI_PROFILES = frozenset(
38
55
  {
39
56
  "internal_admin",
@@ -47,6 +64,14 @@ _UNRESTRICTED_CLI_PROFILES = frozenset(
47
64
  )
48
65
 
49
66
 
67
+ def scopes_for_cli_profile(profile: str) -> frozenset[str]:
68
+ """Resolve one sanctioned CLI profile to its DAL scope set."""
69
+ resolved = _CLI_PROFILE_SCOPES.get(profile)
70
+ if resolved is None:
71
+ raise ValueError(f"Unknown CLI profile: {profile}")
72
+ return resolved
73
+
74
+
50
75
  @dataclass(frozen=True)
51
76
  class AdminConnectionConfig:
52
77
  """Describe the canonical admin connection the ops-plane CLI should prefer.
@@ -281,7 +306,7 @@ async def get_cli_context(
281
306
  # Test/CI: wrap local connection in context
282
307
  async with _local_connection(settings) as conn:
283
308
  await _stamp_cli_session_contract(conn, profile=resolved_profile)
284
- ctx = await Context.for_cli_profile(conn, profile=resolved_profile)
309
+ ctx = Context.for_cli(conn, scopes=scopes_for_cli_profile(resolved_profile))
285
310
  yield None, ctx
286
311
  else:
287
312
  # Development/Production: use infra.Database
@@ -289,7 +314,7 @@ async def get_cli_context(
289
314
  try:
290
315
  await db.connect()
291
316
  await _stamp_cli_session_contract(db.conn, profile=resolved_profile)
292
- ctx = await Context.for_cli_profile(db, profile=resolved_profile)
317
+ ctx = Context.for_cli(db, scopes=scopes_for_cli_profile(resolved_profile))
293
318
  yield db, ctx
294
319
  finally:
295
320
  await db.close()
@@ -7,20 +7,28 @@ import typer
7
7
  from cli.console import error
8
8
 
9
9
 
10
- def require_write(ctx: typer.Context, action: str) -> None:
10
+ def require_write(
11
+ ctx: typer.Context,
12
+ action: str,
13
+ *,
14
+ allowed_profiles: frozenset[str] | None = None,
15
+ ) -> None:
11
16
  """
12
17
  Enforce --write for mutating commands.
13
18
 
14
19
  Args:
15
20
  ctx: Typer context
16
21
  action: Short description of the mutation (for error messages)
22
+ allowed_profiles: CLI profiles allowed to perform the mutation
17
23
  """
18
24
  ctx_obj = ctx.obj or {}
19
25
  allow_write = ctx_obj.get("allow_write", False)
20
26
  if not allow_write:
21
27
  error(f"{action} is a write operation. Re-run with --write.")
22
28
  raise typer.Exit(1)
23
- cli_profile = ctx_obj.get("cli_profile", "internal_viewer")
24
- if cli_profile != "internal_admin":
25
- error(f"{action} requires --profile internal_admin.")
29
+ cli_profile = ctx_obj.get("cli_profile") or "internal_viewer"
30
+ required_profiles = allowed_profiles or frozenset({"internal_admin"})
31
+ if cli_profile not in required_profiles:
32
+ profile_list = ", ".join(sorted(required_profiles))
33
+ error(f"{action} requires one of these profiles: {profile_list}.")
26
34
  raise typer.Exit(1)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.45"
7
+ version = "0.3.47"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes