From 1ba2939e0bffb71ab4964c38c4848bb81a5297b4 Mon Sep 17 00:00:00 2001 From: Priyesh Srivastava Date: Sun, 10 May 2026 14:40:52 +0530 Subject: [PATCH 1/2] Add DB sandbox provider preflight --- .circleci/config.yml | 14 + .github/workflows/ci.yml | 14 + README.md | 39 +- docs/config.md | 47 +- scripts/probes/README.md | 17 +- scripts/probes/check-db-sandbox-readiness.sh | 56 +- .../probes/db-sandbox-readiness.env.example | 35 +- thinwedge-rs/cli/src/db_sandbox_cmd.rs | 603 ++++++++++++++++++ thinwedge-rs/cli/src/lib.rs | 3 + thinwedge-rs/cli/src/login.rs | 6 +- thinwedge-rs/cli/src/main.rs | 18 + thinwedge-rs/config/src/config_toml.rs | 5 + thinwedge-rs/config/src/types.rs | 49 ++ thinwedge-rs/config/src/types_tests.rs | 23 + thinwedge-rs/core/config.schema.json | 111 ++++ 15 files changed, 988 insertions(+), 52 deletions(-) create mode 100644 thinwedge-rs/cli/src/db_sandbox_cmd.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index ae85b4a7c..38c48ef7b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,6 +105,20 @@ jobs: echo "tag ${tag_ver} does not match Cargo.toml ${cargo_ver}" exit 1 fi + - run: + name: DB sandbox release smoke + command: | + set -euo pipefail + scripts/probes/check-db-sandbox-readiness.sh --dry-run + scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none --include-branch-lifecycle --dry-run + scripts/probes/check-db-sandbox-readiness.sh --source-provider planetscale --branch-backend none --dry-run + python3 - <<'PY' + import json + schema = json.load(open("thinwedge-rs/core/config.schema.json", encoding="utf-8")) + props = schema.get("properties", {}) + assert "db_sandbox" in props, "config schema must expose db_sandbox" + assert "ardent" in props, "config schema must keep optional Ardent settings" + PY build-linux-x64: machine: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4204da595..1e138584c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,20 @@ jobs: - name: Check public release readiness blockers run: python3 scripts/check_public_release_readiness.py + - name: DB sandbox release smoke + run: | + set -euo pipefail + scripts/probes/check-db-sandbox-readiness.sh --dry-run + scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none --include-branch-lifecycle --dry-run + scripts/probes/check-db-sandbox-readiness.sh --source-provider planetscale --branch-backend none --dry-run + python3 - <<'PY' + import json + schema = json.load(open("thinwedge-rs/core/config.schema.json", encoding="utf-8")) + props = schema.get("properties", {}) + assert "db_sandbox" in props, "config schema must expose db_sandbox" + assert "ardent" in props, "config schema must keep optional Ardent settings" + PY + - name: Setup pnpm uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 with: diff --git a/README.md b/README.md index 2bfd54d21..ba6a72b43 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ npm install -g @never2average-does-npm/cli Authenticate before starting interactive mode. In a real terminal, `thinwedge login` prompts for the required OpenRouter-compatible API token and optional capability -config such as Artificial Analysis, RunPod, and AWS profile/region values: +config such as Artificial Analysis, RunPod, AWS profile/region values, and Neon +DB sandbox metadata: ```shell thinwedge login @@ -24,6 +25,8 @@ thinwedge login # RUNPOD_API_KEY (...) [optional]: ... # AWS_PROFILE (...) [optional]: ... # AWS_REGION (...) [optional]: ... +# THINWEDGE_NEON_API_KEY (...) [optional]: ... +# THINWEDGE_NEON_PROJECT_ID (...) [optional]: ... ``` Or set the provider token in the environment first: @@ -61,6 +64,7 @@ The product surface is intentionally local-first: - `thinwedge` starts the interactive terminal UI. - `thinwedge exec` runs a single non-interactive agent task. - `thinwedge login` stores an OpenRouter-compatible API token locally. +- `thinwedge db-sandbox` configures and preflights DB sandbox providers. - `THINWEDGE_HOME` controls where config, auth, logs, thread history, and local state live. ## System Components @@ -97,7 +101,8 @@ ThinWedge uses API-token authentication. Run `thinwedge login` before starting the interactive TUI so the agent can call the configured provider API. In a real terminal, `thinwedge login` behaves like an `aws configure`-style prompt: it asks for the required OpenRouter-compatible API token, then offers optional prompts for -`ARTIFICIAL_ANALYSIS_API_KEY`, `RUNPOD_API_KEY`, `AWS_PROFILE`, and `AWS_REGION`. +`ARTIFICIAL_ANALYSIS_API_KEY`, `RUNPOD_API_KEY`, `AWS_PROFILE`, `AWS_REGION`, +`THINWEDGE_NEON_API_KEY`, and `THINWEDGE_NEON_PROJECT_ID`. The required provider token is stored in ThinWedge auth storage; optional capability values are written to `THINWEDGE_HOME/.env`, which ThinWedge loads on startup. @@ -108,6 +113,8 @@ thinwedge login # RUNPOD_API_KEY (...) [optional]: ... # AWS_PROFILE (...) [optional]: ... # AWS_REGION (...) [optional]: ... +# THINWEDGE_NEON_API_KEY (...) [optional]: ... +# THINWEDGE_NEON_PROJECT_ID (...) [optional]: ... ``` You can also provide the provider token through the environment: @@ -143,6 +150,34 @@ coordination model: into real workbook sheets. - `/status`, `/diff`, `/permissions`, `/mcp`, `/skills`, `/apps`, and `/plugins` expose runtime, workspace, and integration state. +## DB Sandbox Setup + +Finance agents should not run migrations or experiments directly against +production state. ThinWedge models this as a provider-first setup: validate a +source provider, then hand agents only disposable database state. + +Neon is the default path: + +```shell +thinwedge db-sandbox configure --enabled --provider neon --neon-project-id --branch-backend none +thinwedge db-sandbox preflight --dry-run +thinwedge db-sandbox preflight --provider neon +``` + +Ardent is optional. Use it as the branch backend only after the Neon or Postgres +source checks pass: + +```shell +thinwedge db-sandbox configure --branch-backend ardent +thinwedge ardent status --dry-run +``` + +The bottom-up source of truth is still the probe scripts: + +```shell +scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none +``` + ## Logical Tool Tree ThinWedge organizes tools in layers so the agent can reason about local execution, diff --git a/docs/config.md b/docs/config.md index 7afafd403..12fad0079 100644 --- a/docs/config.md +++ b/docs/config.md @@ -72,10 +72,11 @@ The generated JSON Schema for `config.toml` lives at `thinwedge-rs/core/config.s ## Finance DB sandboxing -ThinWedge can keep finance/database agents away from production state by using an -Ardent Postgres branch as the task database. The CLI integration is optional: -`thinwedge login` can offer to configure it, and explicit `thinwedge ardent ...` -commands can manage it directly. +ThinWedge can keep finance/database agents away from production state by first +validating a source provider, then handing agents only disposable sandbox +database state. Neon is the default provider path because it can be checked from +a Postgres URL plus Neon API metadata. Ardent remains optional: use it when you +want an external branch backend after the source provider is proven. The non-secret configuration shape is: @@ -90,6 +91,15 @@ aws_profile = "fpna-db-ops" role_arn = "arn:aws:iam::123456789012:role/fpna-db-ops" region = "us-west-2" +[db_sandbox] +enabled = true +provider = "neon" +source_url_env = "THINWEDGE_ARDENT_SOURCE_DATABASE_URL" +neon_api_key_env = "THINWEDGE_NEON_API_KEY" +neon_project_id_env = "THINWEDGE_NEON_PROJECT_ID" +neon_project_id = "twilight-lab-63846303" +branch_backend = "none" + [ardent] enabled = true cli_path = "ardent" @@ -101,13 +111,17 @@ data_plane = "byoc" Use `aws_profile` for local workstation setup. Production deployments should prefer role-based or managed credential providers that resolve to narrowly scoped -AWS credentials. Source database URLs and Ardent branch URLs are not config -values; source credentials should come from secure stores, and agents should only -receive a branch `DATABASE_URL`. +AWS credentials. Source database URLs, API keys, and branch URLs are not config +values; they should come from secure stores or environment variables. Agents +should only receive disposable branch or sandbox `DATABASE_URL` values. Useful CLI entry points: ```bash +thinwedge db-sandbox status +thinwedge db-sandbox configure --enabled --provider neon --neon-project-id --branch-backend none --dry-run +thinwedge db-sandbox preflight --dry-run +thinwedge db-sandbox preflight --provider neon thinwedge ardent status --dry-run thinwedge ardent login --dry-run thinwedge ardent configure --enabled --billing-profile fpna-billing --billing-role-arn arn:aws:iam::123456789012:role/fpna-billing --db-ops-profile fpna-db-ops --db-ops-role-arn arn:aws:iam::123456789012:role/fpna-db-ops --connector fpna-prod --data-plane byoc --dry-run --no-prompt @@ -117,6 +131,12 @@ thinwedge ardent branch create --connector fpna-prod --name thinwedge-agent-test thinwedge ardent branch delete thinwedge-agent-test --connector fpna-prod --dry-run ``` +`thinwedge db-sandbox configure` stores only non-secret provider metadata. The +interactive `thinwedge login` flow offers the same setup after the OpenRouter API +token and optional capability prompts. `branch_backend = "none"` is the default +for provider-only validation; switch it to `"ardent"` only after deciding Ardent +should manage disposable database branches. + `thinwedge ardent connector create` is intentionally mutation-gated for live runs because it attaches a production source database to Ardent. Pass `--allow-mutation` only after that blast radius is approved. The source URL is @@ -128,8 +148,10 @@ Choosing `managed` versus `byoc` for `ardent.data_plane` is also an explicit deployment-boundary decision; record that choice during setup instead of treating it as an automatic default. -Validate the external contracts bottom-up before trusting the integration. Start -with dry-run wiring: +Validate the external contracts bottom-up before trusting the integration. This +is the production-scale workflow for a technical CFO/operator: bring a source DB +URL/API key, run preflight, and only then allow agents to use disposable DB +state. Start with dry-run wiring: ```bash scripts/probes/check-db-sandbox-readiness.sh --dry-run @@ -145,13 +167,14 @@ set -a . scripts/probes/db-sandbox-readiness.env set +a -scripts/probes/check-db-sandbox-readiness.sh +scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none ``` -Live branch creation/deletion is mutation-gated and requires explicit opt-in: +Live branch creation/deletion is mutation-gated and requires explicit opt-in. +Use this only when `branch_backend = "ardent"` is part of the deployment: ```bash -TW_PROBE_ALLOW_MUTATION=1 scripts/probes/check-ardent-branch-lifecycle.sh +TW_PROBE_ALLOW_MUTATION=1 scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend ardent --include-branch-lifecycle --allow-mutation ``` For Neon, validate the source and Neon API before attempting Ardent BYOC setup: diff --git a/scripts/probes/README.md b/scripts/probes/README.md index bde240a02..77646dc04 100644 --- a/scripts/probes/README.md +++ b/scripts/probes/README.md @@ -1,8 +1,8 @@ # ThinWedge Probe Scripts -These scripts validate external integration contracts before the Rust CLI is rebuilt. They are intentionally bottom-up so AWS billing, AWS DB Ops, source database readiness, Ardent auth, connector readiness, and branch lifecycle can be tested independently. +These scripts validate external integration contracts before the Rust CLI is rebuilt. They are intentionally bottom-up so provider readiness, optional AWS billing, optional AWS DB Ops, optional Ardent auth, connector readiness, and branch lifecycle can be tested independently. -All probes support `--dry-run` for cheap syntax and wiring checks. Live runs require the relevant AWS, Neon, database, and Ardent credentials in the current shell. Set `THINWEDGE_DB_SECRET_ID` or `THINWEDGE_DB_SSM_PARAMETER` to verify a specific DB connection secret. RDS readiness live runs require TCP reachability from the current host and `THINWEDGE_DB_ROLE_DATABASE_URL` for the DB setup role check. Generic Postgres, Supabase, and Neon readiness live runs require `THINWEDGE_ARDENT_SOURCE_DATABASE_URL`. +All probes support `--dry-run` for cheap syntax and wiring checks. Live runs require the relevant provider credentials in the current shell. Neon is the default provider path; Ardent is checked only when `--branch-backend ardent` or `THINWEDGE_DB_SANDBOX_BRANCH_BACKEND=ardent` is set. Set `THINWEDGE_DB_SECRET_ID` or `THINWEDGE_DB_SSM_PARAMETER` to verify a specific DB connection secret. RDS readiness live runs require TCP reachability from the current host and `THINWEDGE_DB_ROLE_DATABASE_URL` for the DB setup role check. Generic Postgres, Supabase, and Neon readiness live runs require `THINWEDGE_ARDENT_SOURCE_DATABASE_URL`. ```bash scripts/probes/check-aws-billing.sh --dry-run @@ -18,15 +18,15 @@ scripts/probes/check-db-sandbox-readiness.sh --dry-run Live validation expects the shell to already have: - an AWS CLI on `PATH` -- a billing AWS identity with STS, Cost Explorer, CUR, Budgets, and IAM account-summary read access, via `THINWEDGE_BILLING_AWS_PROFILE` or `AWS_PROFILE` -- a DB Ops AWS identity, via `THINWEDGE_DB_OPS_AWS_PROFILE` or `AWS_PROFILE` +- a billing AWS identity with STS, Cost Explorer, CUR, Budgets, and IAM account-summary read access, via `THINWEDGE_BILLING_AWS_PROFILE` or `AWS_PROFILE`, when running billing checks +- a DB Ops AWS identity, via `THINWEDGE_DB_OPS_AWS_PROFILE` or `AWS_PROFILE`, when running RDS or DB Ops checks - `nc` for RDS TCP reachability checks - `psql` plus `THINWEDGE_DB_ROLE_DATABASE_URL` for RDS DB setup role validation - `psql` plus `THINWEDGE_ARDENT_SOURCE_DATABASE_URL` for generic Postgres, Supabase, and Neon source validation - a Neon API key and project id in `THINWEDGE_NEON_API_KEY` and `THINWEDGE_NEON_PROJECT_ID` when `THINWEDGE_DB_SOURCE_PROVIDER=neon` -- an authenticated Ardent CLI from `ardent login` -- a selected Ardent project from `ardent project switch ` -- at least one Ardent connector; set `THINWEDGE_ARDENT_CONNECTOR` to verify the intended connector +- an authenticated Ardent CLI from `ardent login`, only when using `--branch-backend ardent` +- a selected Ardent project from `ardent project switch `, only when using `--branch-backend ardent` +- at least one Ardent connector; set `THINWEDGE_ARDENT_CONNECTOR` to verify the intended connector, only when using `--branch-backend ardent` Run the non-mutating live checks before trusting a finance DB sandbox setup: @@ -37,6 +37,7 @@ set -a . scripts/probes/db-sandbox-readiness.env set +a +scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none scripts/probes/check-aws-billing.sh scripts/probes/check-aws-db-ops.sh scripts/probes/check-rds-postgres-readiness.sh --db-instance @@ -44,7 +45,7 @@ THINWEDGE_DB_SOURCE_PROVIDER=postgresql scripts/probes/check-postgres-source-rea THINWEDGE_DB_SOURCE_PROVIDER=neon scripts/probes/check-neon-postgres-readiness.sh scripts/probes/check-ardent-auth.sh scripts/probes/check-ardent-connector.sh --connector -scripts/probes/check-db-sandbox-readiness.sh +scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend ardent ``` The RDS readiness probe fails live unless it can prove network reachability and diff --git a/scripts/probes/check-db-sandbox-readiness.sh b/scripts/probes/check-db-sandbox-readiness.sh index 123665683..e27b91de5 100755 --- a/scripts/probes/check-db-sandbox-readiness.sh +++ b/scripts/probes/check-db-sandbox-readiness.sh @@ -4,15 +4,17 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <<'EOF' -Usage: check-db-sandbox-readiness.sh [--source-provider rds|postgresql|supabase|neon] [--dry-run] [--include-branch-lifecycle] [--allow-mutation] +Usage: check-db-sandbox-readiness.sh [--source-provider neon|postgresql|rds|supabase|planetscale] [--branch-backend none|ardent] [--dry-run] [--include-branch-lifecycle] [--allow-mutation] Runs the bottom-up readiness probes for ThinWedge finance DB sandboxing. By -default it avoids mutation-gated Ardent branch creation. Use both +default it validates Neon provider readiness without requiring Ardent. Use +--branch-backend ardent to add Ardent auth/connector checks. Use both --include-branch-lifecycle and --allow-mutation after explicit approval to create -and delete a temporary Ardent branch. +and delete a temporary provider or backend branch. Common env vars: THINWEDGE_DB_SOURCE_PROVIDER + THINWEDGE_DB_SANDBOX_BRANCH_BACKEND THINWEDGE_BILLING_AWS_PROFILE THINWEDGE_DB_OPS_AWS_PROFILE THINWEDGE_RDS_DB_INSTANCE @@ -27,10 +29,12 @@ EOF dry_run=0 include_branch_lifecycle=0 allow_mutation=0 -source_provider="${THINWEDGE_DB_SOURCE_PROVIDER:-rds}" +source_provider="${THINWEDGE_DB_SOURCE_PROVIDER:-neon}" +branch_backend="${THINWEDGE_DB_SANDBOX_BRANCH_BACKEND:-none}" while [[ $# -gt 0 ]]; do case "$1" in --source-provider) source_provider="${2:?missing source provider}"; shift 2 ;; + --branch-backend) branch_backend="${2:?missing branch backend}"; shift 2 ;; --dry-run) dry_run=1; shift ;; --include-branch-lifecycle) include_branch_lifecycle=1; shift ;; --allow-mutation) allow_mutation=1; shift ;; @@ -44,9 +48,19 @@ args=() mutation_args=() [[ "${dry_run}" == "1" ]] && mutation_args+=(--dry-run) [[ "${allow_mutation}" == "1" ]] && mutation_args+=(--allow-mutation) +neon_args=("${mutation_args[@]}") +[[ "${include_branch_lifecycle}" == "1" ]] && neon_args+=(--include-api-branch-smoke) -"${SCRIPT_DIR}/check-aws-billing.sh" "${args[@]}" -"${SCRIPT_DIR}/check-aws-db-ops.sh" "${args[@]}" +if [[ -n "${THINWEDGE_BILLING_AWS_PROFILE:-}" || -n "${THINWEDGE_BILLING_ROLE_ARN:-}" ]]; then + "${SCRIPT_DIR}/check-aws-billing.sh" "${args[@]}" +else + echo '[thinwedge-probe] skipping AWS billing probe; no billing AWS identity env configured' +fi +if [[ "${source_provider}" == "rds" || -n "${THINWEDGE_DB_OPS_AWS_PROFILE:-}" || -n "${THINWEDGE_DB_OPS_ROLE_ARN:-}" ]]; then + "${SCRIPT_DIR}/check-aws-db-ops.sh" "${args[@]}" +else + echo '[thinwedge-probe] skipping AWS DB Ops probe; provider does not require AWS DB Ops env' +fi case "${source_provider}" in rds) "${SCRIPT_DIR}/check-rds-postgres-readiness.sh" "${args[@]}" @@ -55,7 +69,10 @@ case "${source_provider}" in "${SCRIPT_DIR}/check-postgres-source-readiness.sh" "${mutation_args[@]}" ;; neon) - "${SCRIPT_DIR}/check-neon-postgres-readiness.sh" "${mutation_args[@]}" + "${SCRIPT_DIR}/check-neon-postgres-readiness.sh" "${neon_args[@]}" + ;; + planetscale) + echo '[thinwedge-probe] PlanetScale provider selected; live PlanetScale probe is not available yet' ;; none|skip) echo '[thinwedge-probe] skipping source database readiness probe by explicit provider' @@ -65,11 +82,22 @@ case "${source_provider}" in exit 1 ;; esac -"${SCRIPT_DIR}/check-ardent-auth.sh" "${args[@]}" -"${SCRIPT_DIR}/check-ardent-connector.sh" "${args[@]}" -if [[ "${include_branch_lifecycle}" == "1" ]]; then - "${SCRIPT_DIR}/check-ardent-branch-lifecycle.sh" "${mutation_args[@]}" -else - echo '[thinwedge-probe] skipping mutation-gated Ardent branch lifecycle probe' -fi +case "${branch_backend}" in + none|skip) + echo '[thinwedge-probe] skipping branch backend probe by explicit backend' + ;; + ardent) + "${SCRIPT_DIR}/check-ardent-auth.sh" "${args[@]}" + "${SCRIPT_DIR}/check-ardent-connector.sh" "${args[@]}" + if [[ "${include_branch_lifecycle}" == "1" ]]; then + "${SCRIPT_DIR}/check-ardent-branch-lifecycle.sh" "${mutation_args[@]}" + else + echo '[thinwedge-probe] skipping mutation-gated Ardent branch lifecycle probe' + fi + ;; + *) + echo "unknown branch backend: ${branch_backend}" >&2 + exit 1 + ;; +esac echo '[thinwedge-probe] DB sandbox readiness probe suite passed' diff --git a/scripts/probes/db-sandbox-readiness.env.example b/scripts/probes/db-sandbox-readiness.env.example index c62ba2999..174679851 100644 --- a/scripts/probes/db-sandbox-readiness.env.example +++ b/scripts/probes/db-sandbox-readiness.env.example @@ -1,17 +1,24 @@ # Copy to scripts/probes/db-sandbox-readiness.env and fill locally. # Do not commit the copied file; it may contain a source DB setup-role URL. -# Local workstation profiles are acceptable for probe validation. Production -# should resolve these identities through roles or another managed provider. -export THINWEDGE_BILLING_AWS_PROFILE="fpna-billing" -export THINWEDGE_DB_OPS_AWS_PROFILE="fpna-db-ops" - # Source provider routing for check-db-sandbox-readiness.sh: -# rds - AWS RDS Postgres metadata + source-role checks +# neon - Neon API checks + generic Postgres source URL checks # postgresql - generic Postgres source URL checks # supabase - generic Postgres source URL checks; Ardent still requires IPv4 -# neon - Neon API checks + generic Postgres source URL checks -export THINWEDGE_DB_SOURCE_PROVIDER="rds" +# rds - AWS RDS Postgres metadata + source-role checks +# planetscale - records provider intent; live probe support is not available yet +export THINWEDGE_DB_SOURCE_PROVIDER="neon" + +# Branch backend routing: +# none - validate source/provider only +# ardent - also validate Ardent auth/connector and optional branch lifecycle +export THINWEDGE_DB_SANDBOX_BRANCH_BACKEND="none" + +# Local workstation profiles are acceptable for probe validation. Production +# should resolve these identities through roles or another managed provider. +# Required only for RDS source checks or if you want AWS billing/DB Ops probes. +# export THINWEDGE_BILLING_AWS_PROFILE="fpna-billing" +# export THINWEDGE_DB_OPS_AWS_PROFILE="fpna-db-ops" # RDS Postgres source metadata. export AWS_REGION="us-east-1" @@ -22,16 +29,16 @@ export THINWEDGE_RDS_DB_INSTANCE="" export THINWEDGE_DB_ROLE_DATABASE_URL="" export THINWEDGE_ARDENT_SOURCE_DATABASE_URL="" -# Neon BYOC source metadata. Required only when THINWEDGE_DB_SOURCE_PROVIDER=neon -# or when creating an Ardent BYOC Neon connector. -# export THINWEDGE_NEON_API_KEY="" -# export THINWEDGE_NEON_PROJECT_ID="" +# Neon source metadata. Required when THINWEDGE_DB_SOURCE_PROVIDER=neon. +export THINWEDGE_NEON_API_KEY="" +export THINWEDGE_NEON_PROJECT_ID="" # Optional: verify a specific DB secret/parameter instead of only list access. # export THINWEDGE_DB_SECRET_ID="" # export THINWEDGE_DB_SSM_PARAMETER="" -# Ardent CLI state must already be authenticated with: +# Ardent CLI state is required only when THINWEDGE_DB_SANDBOX_BRANCH_BACKEND=ardent. +# It must already be authenticated with: # ardent login # ardent project switch -export THINWEDGE_ARDENT_CONNECTOR="" +# export THINWEDGE_ARDENT_CONNECTOR="" diff --git a/thinwedge-rs/cli/src/db_sandbox_cmd.rs b/thinwedge-rs/cli/src/db_sandbox_cmd.rs new file mode 100644 index 000000000..b1b9a58fb --- /dev/null +++ b/thinwedge-rs/cli/src/db_sandbox_cmd.rs @@ -0,0 +1,603 @@ +use anyhow::Context; +use clap::Parser; +use clap::ValueEnum; +use std::io::IsTerminal; +use std::io::Write; +use std::process::Command; +use thinwedge_config::config_toml::ConfigToml; +use thinwedge_config::types::ArdentConfigToml; +use thinwedge_config::types::AwsIdentityConfigToml; +use thinwedge_config::types::DbSandboxConfigToml; +use thinwedge_core::config::Config; +use thinwedge_core::config::edit::ConfigEdit; +use thinwedge_core::config::edit::ConfigEditsBuilder; +use thinwedge_core::config::find_thinwedge_home; +use thinwedge_utils_cli::CliConfigOverrides; +use toml_edit::Item as TomlItem; +use toml_edit::value; + +const DEFAULT_PROVIDER: DbSandboxProviderArg = DbSandboxProviderArg::Neon; +const DEFAULT_SOURCE_URL_ENV: &str = "THINWEDGE_ARDENT_SOURCE_DATABASE_URL"; +const DEFAULT_NEON_API_KEY_ENV: &str = "THINWEDGE_NEON_API_KEY"; +const DEFAULT_NEON_PROJECT_ID_ENV: &str = "THINWEDGE_NEON_PROJECT_ID"; + +#[derive(Debug, Parser)] +#[command(bin_name = "thinwedge db-sandbox")] +pub struct DbSandboxCli { + #[clap(skip)] + pub config_overrides: CliConfigOverrides, + + #[command(subcommand)] + command: DbSandboxSubcommand, +} + +#[derive(Debug, clap::Subcommand)] +enum DbSandboxSubcommand { + /// Show ThinWedge DB sandbox configuration and the matching probe command. + Status(DbSandboxStatusCommand), + /// Save non-secret DB sandbox setup. + Configure(DbSandboxConfigureCommand), + /// Run the bottom-up DB sandbox probe script when available. + Preflight(DbSandboxPreflightCommand), +} + +#[derive(Debug, Parser)] +struct DbSandboxStatusCommand { + /// Show the equivalent probe command without running anything. + #[arg(long)] + dry_run: bool, +} + +#[derive(Debug, Parser, Default)] +struct DbSandboxConfigureCommand { + /// Enable DB sandbox setup in config.toml. + #[arg(long = "enabled", conflicts_with = "disabled")] + enable: bool, + + /// Disable DB sandbox setup in config.toml. + #[arg(long = "disabled", conflicts_with = "enabled")] + disable: bool, + + /// Do not prompt for missing values. + #[arg(long)] + no_prompt: bool, + + /// Show the config edits that would be saved. + #[arg(long)] + dry_run: bool, + + /// Source DB provider. Defaults to Neon when omitted in interactive setup. + #[arg(long, value_enum)] + provider: Option, + + /// Env var that contains the source Postgres URL for URL-based providers. + #[arg(long)] + source_url_env: Option, + + /// Env var that contains a Neon API key. + #[arg(long)] + neon_api_key_env: Option, + + /// Env var that contains the Neon project id. + #[arg(long)] + neon_project_id_env: Option, + + /// Optional non-secret Neon project id. + #[arg(long)] + neon_project_id: Option, + + /// Optional branch backend. Use `ardent` only after deciding to connect Ardent. + #[arg(long, value_enum)] + branch_backend: Option, +} + +#[derive(Debug, Parser)] +struct DbSandboxPreflightCommand { + /// Source provider to probe. Defaults to config or Neon. + #[arg(long, value_enum)] + provider: Option, + + /// Include mutation-gated branch lifecycle checks when backend supports them. + #[arg(long)] + include_branch_lifecycle: bool, + + /// Allow mutation-gated probe steps. Requires explicit approval. + #[arg(long)] + allow_mutation: bool, + + /// Print the probe command without running it. + #[arg(long)] + dry_run: bool, +} + +#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)] +enum DbSandboxProviderArg { + Neon, + Postgresql, + Rds, + Supabase, + Planetscale, +} + +impl DbSandboxProviderArg { + fn as_config_value(self) -> &'static str { + match self { + Self::Neon => "neon", + Self::Postgresql => "postgresql", + Self::Rds => "rds", + Self::Supabase => "supabase", + Self::Planetscale => "planetscale", + } + } + + fn from_config(value: Option) -> Self { + match value { + Some(thinwedge_config::types::DbSandboxProviderToml::Postgresql) => Self::Postgresql, + Some(thinwedge_config::types::DbSandboxProviderToml::Rds) => Self::Rds, + Some(thinwedge_config::types::DbSandboxProviderToml::Supabase) => Self::Supabase, + Some(thinwedge_config::types::DbSandboxProviderToml::Planetscale) => Self::Planetscale, + Some(thinwedge_config::types::DbSandboxProviderToml::Neon) | None => Self::Neon, + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)] +enum DbSandboxBackendArg { + None, + Ardent, +} + +impl DbSandboxBackendArg { + fn as_config_value(self) -> &'static str { + match self { + Self::None => "none", + Self::Ardent => "ardent", + } + } + + fn from_config(value: Option) -> Self { + match value { + Some(thinwedge_config::types::DbSandboxBackendToml::Ardent) => Self::Ardent, + Some(thinwedge_config::types::DbSandboxBackendToml::None) | None => Self::None, + } + } +} + +#[derive(Debug)] +struct LoadedDbSandboxConfig { + billing: Option, + db_ops: Option, + db_sandbox: Option, + ardent: Option, +} + +#[derive(Default)] +struct DbSandboxConfigUpdates { + enabled: Option, + provider: Option, + source_url_env: Option, + neon_api_key_env: Option, + neon_project_id_env: Option, + neon_project_id: Option, + branch_backend: Option, +} + +impl DbSandboxConfigUpdates { + fn into_edits(self) -> Vec { + let mut edits = Vec::new(); + push_bool_edit(&mut edits, &["db_sandbox", "enabled"], self.enabled); + push_string_edit(&mut edits, &["db_sandbox", "provider"], self.provider); + push_string_edit( + &mut edits, + &["db_sandbox", "source_url_env"], + self.source_url_env, + ); + push_string_edit( + &mut edits, + &["db_sandbox", "neon_api_key_env"], + self.neon_api_key_env, + ); + push_string_edit( + &mut edits, + &["db_sandbox", "neon_project_id_env"], + self.neon_project_id_env, + ); + push_string_edit( + &mut edits, + &["db_sandbox", "neon_project_id"], + self.neon_project_id, + ); + push_string_edit( + &mut edits, + &["db_sandbox", "branch_backend"], + self.branch_backend, + ); + edits + } +} + +pub async fn run_db_sandbox_cli(cli: DbSandboxCli) -> anyhow::Result<()> { + let cli_overrides = cli + .config_overrides + .parse_overrides() + .map_err(anyhow::Error::msg)?; + match cli.command { + DbSandboxSubcommand::Status(cmd) => run_status(cli_overrides, cmd).await, + DbSandboxSubcommand::Configure(cmd) => run_configure(cmd).await, + DbSandboxSubcommand::Preflight(cmd) => run_preflight(cli_overrides, cmd).await, + } +} + +async fn run_status( + cli_overrides: Vec<(String, toml::Value)>, + cmd: DbSandboxStatusCommand, +) -> anyhow::Result<()> { + let loaded = load_db_sandbox_config(cli_overrides).await?; + let provider = + DbSandboxProviderArg::from_config(loaded.db_sandbox.as_ref().and_then(|cfg| cfg.provider)); + let backend = DbSandboxBackendArg::from_config( + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.branch_backend), + ); + println!("ThinWedge DB sandbox config:"); + println!( + " db_sandbox.enabled: {}", + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.enabled) + .unwrap_or(false) + ); + println!(" db_sandbox.provider: {}", provider.as_config_value()); + println!(" db_sandbox.branch_backend: {}", backend.as_config_value()); + print_env_ref( + "db_sandbox.source_url_env", + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.source_url_env.as_deref()) + .unwrap_or(DEFAULT_SOURCE_URL_ENV), + ); + print_env_ref( + "db_sandbox.neon_api_key_env", + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.neon_api_key_env.as_deref()) + .unwrap_or(DEFAULT_NEON_API_KEY_ENV), + ); + print_env_ref( + "db_sandbox.neon_project_id_env", + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.neon_project_id_env.as_deref()) + .unwrap_or(DEFAULT_NEON_PROJECT_ID_ENV), + ); + println!( + " db_sandbox.neon_project_id: {}", + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.neon_project_id.as_deref()) + .unwrap_or("") + ); + print_identity_summary("billing", loaded.billing.as_ref()); + print_identity_summary("db_ops", loaded.db_ops.as_ref()); + println!( + " ardent.enabled: {}", + loaded + .ardent + .as_ref() + .and_then(|cfg| cfg.enabled) + .unwrap_or(false) + ); + println!( + " ardent.default_connector: {}", + loaded + .ardent + .as_ref() + .and_then(|cfg| cfg.default_connector.as_deref()) + .unwrap_or("") + ); + let plan = preflight_plan(provider, backend, false, false); + if cmd.dry_run { + println!("dry-run: {}", plan.join(" ")); + } else { + println!("preflight: {}", plan.join(" ")); + } + Ok(()) +} + +async fn run_configure(cmd: DbSandboxConfigureCommand) -> anyhow::Result<()> { + let thinwedge_home = find_thinwedge_home()?.to_path_buf(); + let mut edits = db_sandbox_edits_from_configure_args(&cmd); + if edits.is_empty() && !cmd.no_prompt && std::io::stdin().is_terminal() { + edits = prompt_database_sandbox_config_edits()?; + } + if edits.is_empty() { + println!("No DB sandbox config changes requested."); + return Ok(()); + } + if cmd.dry_run { + println!("Would save {} DB sandbox config value(s).", edits.len()); + return Ok(()); + } + ConfigEditsBuilder::new(&thinwedge_home) + .with_edits(edits) + .apply() + .await?; + println!( + "Saved DB sandbox config to {}", + thinwedge_home.join("config.toml").display() + ); + Ok(()) +} + +async fn run_preflight( + cli_overrides: Vec<(String, toml::Value)>, + cmd: DbSandboxPreflightCommand, +) -> anyhow::Result<()> { + let loaded = load_db_sandbox_config(cli_overrides).await?; + let provider = cmd.provider.unwrap_or_else(|| { + DbSandboxProviderArg::from_config(loaded.db_sandbox.as_ref().and_then(|cfg| cfg.provider)) + }); + let backend = DbSandboxBackendArg::from_config( + loaded + .db_sandbox + .as_ref() + .and_then(|cfg| cfg.branch_backend), + ); + let plan = preflight_plan( + provider, + backend, + cmd.include_branch_lifecycle, + cmd.allow_mutation, + ); + let script = std::path::Path::new(&plan[0]); + if cmd.dry_run { + println!("dry-run: {}", plan.join(" ")); + return Ok(()); + } + if !script.exists() { + println!( + "Probe script not found in this working tree. Run from the repository root:\n{}", + plan.join(" ") + ); + return Ok(()); + } + let status = Command::new(&plan[0]).args(&plan[1..]).status()?; + if !status.success() { + anyhow::bail!("DB sandbox preflight failed: {}", plan.join(" ")); + } + Ok(()) +} + +fn db_sandbox_edits_from_configure_args(cmd: &DbSandboxConfigureCommand) -> Vec { + let mut updates = DbSandboxConfigUpdates::default(); + if cmd.enable { + updates.enabled = Some(true); + } + if cmd.disable { + updates.enabled = Some(false); + } + updates.provider = cmd + .provider + .map(DbSandboxProviderArg::as_config_value) + .map(str::to_string); + updates.source_url_env = cmd.source_url_env.clone(); + updates.neon_api_key_env = cmd.neon_api_key_env.clone(); + updates.neon_project_id_env = cmd.neon_project_id_env.clone(); + updates.neon_project_id = cmd.neon_project_id.clone(); + updates.branch_backend = cmd + .branch_backend + .map(DbSandboxBackendArg::as_config_value) + .map(str::to_string); + updates.into_edits() +} + +pub(crate) fn prompt_database_sandbox_config_edits() -> anyhow::Result> { + if !std::io::stdin().is_terminal() { + return Ok(Vec::new()); + } + let answer = prompt_optional("Configure safe database sandboxes for finance agents? [Y/n]")?; + if matches!(answer.as_deref(), Some("n") | Some("no")) { + return Ok(Vec::new()); + } + + let provider = prompt_optional( + "DB sandbox provider neon/postgresql/rds/supabase/planetscale [default: neon]", + )? + .unwrap_or_else(|| DEFAULT_PROVIDER.as_config_value().to_string()); + let provider = match provider.as_str() { + "neon" | "postgresql" | "rds" | "supabase" | "planetscale" => provider, + other => anyhow::bail!("unsupported DB sandbox provider `{other}`"), + }; + let backend = prompt_optional("Branch backend none/ardent [default: none]")? + .unwrap_or_else(|| DbSandboxBackendArg::None.as_config_value().to_string()); + let backend = match backend.as_str() { + "none" | "ardent" => backend, + other => anyhow::bail!("unsupported DB sandbox branch backend `{other}`"), + }; + + let mut updates = DbSandboxConfigUpdates { + enabled: Some(true), + provider: Some(provider), + branch_backend: Some(backend), + ..Default::default() + }; + updates.source_url_env = prompt_value( + "Source Postgres URL env var [default: THINWEDGE_ARDENT_SOURCE_DATABASE_URL]", + )?; + updates.neon_api_key_env = + prompt_value("Neon API key env var [default: THINWEDGE_NEON_API_KEY]")?; + updates.neon_project_id_env = + prompt_value("Neon project id env var [default: THINWEDGE_NEON_PROJECT_ID]")?; + updates.neon_project_id = prompt_value("Neon project id [optional, non-secret]")?; + + Ok(updates.into_edits()) +} + +fn preflight_plan( + provider: DbSandboxProviderArg, + backend: DbSandboxBackendArg, + include_branch_lifecycle: bool, + allow_mutation: bool, +) -> Vec { + let mut plan = vec![ + "scripts/probes/check-db-sandbox-readiness.sh".to_string(), + "--source-provider".to_string(), + provider.as_config_value().to_string(), + ]; + if backend == DbSandboxBackendArg::Ardent { + plan.push("--branch-backend".to_string()); + plan.push("ardent".to_string()); + } else { + plan.push("--branch-backend".to_string()); + plan.push("none".to_string()); + } + if include_branch_lifecycle { + plan.push("--include-branch-lifecycle".to_string()); + } + if allow_mutation { + plan.push("--allow-mutation".to_string()); + } + plan +} + +async fn load_db_sandbox_config( + cli_overrides: Vec<(String, toml::Value)>, +) -> anyhow::Result { + let config = Config::load_with_cli_overrides(cli_overrides).await?; + let config_toml: ConfigToml = config + .config_layer_stack + .effective_config() + .clone() + .try_into() + .context("failed to deserialize effective ThinWedge config")?; + Ok(LoadedDbSandboxConfig { + billing: config_toml.billing, + db_ops: config_toml.db_ops, + db_sandbox: config_toml.db_sandbox, + ardent: config_toml.ardent, + }) +} + +fn print_identity_summary(name: &str, config: Option<&AwsIdentityConfigToml>) { + match config { + Some(config) => { + println!( + " {name}.aws_profile: {}", + config.aws_profile.as_deref().unwrap_or("") + ); + println!( + " {name}.role_arn: {}", + config.role_arn.as_deref().unwrap_or("") + ); + println!( + " {name}.region: {}", + config.region.as_deref().unwrap_or("") + ); + } + None => println!(" {name}: "), + } +} + +fn print_env_ref(name: &str, env_name: &str) { + println!(" {name}: {env_name}"); +} + +fn prompt_optional(prompt: &str) -> anyhow::Result> { + let value = prompt_value(prompt)?; + Ok(value.map(|value| value.to_ascii_lowercase())) +} + +fn prompt_value(prompt: &str) -> anyhow::Result> { + eprint!("{prompt}: "); + std::io::stderr().flush()?; + let mut value = String::new(); + std::io::stdin().read_line(&mut value)?; + let value = value.trim().to_string(); + if value.is_empty() { + Ok(None) + } else { + Ok(Some(value)) + } +} + +fn push_string_edit(edits: &mut Vec, segments: &[&str], val: Option) { + if let Some(val) = val.filter(|val| !val.trim().is_empty()) { + push_set_path_edit(edits, segments, value(val)); + } +} + +fn push_bool_edit(edits: &mut Vec, segments: &[&str], val: Option) { + if let Some(val) = val { + push_set_path_edit(edits, segments, value(val)); + } +} + +fn push_set_path_edit(edits: &mut Vec, segments: &[&str], value: TomlItem) { + edits.push(ConfigEdit::SetPath { + segments: segments + .iter() + .map(|segment| (*segment).to_string()) + .collect(), + value, + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn configure_args_default_to_non_secret_db_sandbox_edits() { + let cmd = DbSandboxConfigureCommand { + enable: true, + provider: Some(DbSandboxProviderArg::Neon), + neon_api_key_env: Some("THINWEDGE_NEON_API_KEY".to_string()), + neon_project_id: Some("twilight-lab-63846303".to_string()), + branch_backend: Some(DbSandboxBackendArg::None), + ..Default::default() + }; + let edits = db_sandbox_edits_from_configure_args(&cmd); + assert_eq!(edits.len(), 5); + } + + #[test] + fn preflight_plan_uses_neon_and_no_backend_by_default() { + let plan = preflight_plan( + DbSandboxProviderArg::Neon, + DbSandboxBackendArg::None, + false, + false, + ); + assert_eq!( + plan, + vec![ + "scripts/probes/check-db-sandbox-readiness.sh", + "--source-provider", + "neon", + "--branch-backend", + "none", + ] + ); + } + + #[test] + fn preflight_plan_can_opt_into_ardent_mutations() { + let plan = preflight_plan( + DbSandboxProviderArg::Neon, + DbSandboxBackendArg::Ardent, + true, + true, + ); + assert!(plan.contains(&"--branch-backend".to_string())); + assert!(plan.contains(&"ardent".to_string())); + assert!(plan.contains(&"--include-branch-lifecycle".to_string())); + assert!(plan.contains(&"--allow-mutation".to_string())); + } +} diff --git a/thinwedge-rs/cli/src/lib.rs b/thinwedge-rs/cli/src/lib.rs index 9fe7a617b..160f870f6 100644 --- a/thinwedge-rs/cli/src/lib.rs +++ b/thinwedge-rs/cli/src/lib.rs @@ -1,4 +1,5 @@ pub(crate) mod ardent_cmd; +pub(crate) mod db_sandbox_cmd; pub(crate) mod debug_sandbox; mod exit_status; pub(crate) mod login; @@ -10,6 +11,8 @@ use thinwedge_utils_cli::CliConfigOverrides; pub use ardent_cmd::ArdentCli; pub use ardent_cmd::run_ardent_cli; +pub use db_sandbox_cmd::DbSandboxCli; +pub use db_sandbox_cmd::run_db_sandbox_cli; pub use debug_sandbox::run_command_under_landlock; pub use debug_sandbox::run_command_under_seatbelt; pub use debug_sandbox::run_command_under_windows; diff --git a/thinwedge-rs/cli/src/login.rs b/thinwedge-rs/cli/src/login.rs index 33580e835..fe0a95fae 100644 --- a/thinwedge-rs/cli/src/login.rs +++ b/thinwedge-rs/cli/src/login.rs @@ -7,7 +7,7 @@ //! into a one-shot CLI command while still producing a durable `thinwedge-login.log` artifact that //! support can request from users. -use crate::ardent_cmd::prompt_database_sandbox_config_edits; +use crate::db_sandbox_cmd::prompt_database_sandbox_config_edits; use std::collections::BTreeMap; use std::fs::OpenOptions; use std::io::IsTerminal; @@ -111,7 +111,7 @@ fn init_login_file_logging(config: &Config) -> Option { fn print_api_token_login_help() { eprintln!( - "ThinWedge uses OpenRouter-compatible API-token authentication.\n\nRequired for chat:\n\n export OPENROUTER_API_KEY=...\n thinwedge login\n\nOr pipe the token directly:\n\n printenv OPENROUTER_API_KEY | thinwedge login --with-api-key\n\nOptional capability keys:\n\n ARTIFICIAL_ANALYSIS_API_KEY LLM market data and cost context\n RUNPOD_API_KEY GPU sandbox lifecycle commands\n AWS_PROFILE/AWS_REGION AWS Bedrock and cost tools\n\nThe token is stored locally in ThinWedge auth storage." + "ThinWedge uses OpenRouter-compatible API-token authentication.\n\nRequired for chat:\n\n export OPENROUTER_API_KEY=...\n thinwedge login\n\nOr pipe the token directly:\n\n printenv OPENROUTER_API_KEY | thinwedge login --with-api-key\n\nOptional capability keys:\n\n ARTIFICIAL_ANALYSIS_API_KEY LLM market data and cost context\n RUNPOD_API_KEY GPU sandbox lifecycle commands\n AWS_PROFILE/AWS_REGION AWS Bedrock and cost tools\n THINWEDGE_NEON_API_KEY Neon DB sandbox provider checks\n THINWEDGE_NEON_PROJECT_ID Neon DB sandbox project id\n\nThe token is stored locally in ThinWedge auth storage." ); } @@ -272,6 +272,8 @@ fn prompt_and_save_optional_capability_config(thinwedge_home: &Path) { ("RUNPOD_API_KEY", "GPU sandbox lifecycle commands"), ("AWS_PROFILE", "AWS Bedrock and AWS cost tools profile"), ("AWS_REGION", "AWS Bedrock and AWS cost tools region"), + ("THINWEDGE_NEON_API_KEY", "Neon DB sandbox provider checks"), + ("THINWEDGE_NEON_PROJECT_ID", "Neon DB sandbox project id"), ]; eprintln!( diff --git a/thinwedge-rs/cli/src/main.rs b/thinwedge-rs/cli/src/main.rs index 5e431c983..f32a0ef9f 100644 --- a/thinwedge-rs/cli/src/main.rs +++ b/thinwedge-rs/cli/src/main.rs @@ -12,12 +12,14 @@ use thinwedge_arg0::arg0_dispatch_or_else; use thinwedge_chatgpt::apply_command::ApplyCommand; use thinwedge_chatgpt::apply_command::run_apply_command; use thinwedge_cli::ArdentCli; +use thinwedge_cli::DbSandboxCli; use thinwedge_cli::LandlockCommand; use thinwedge_cli::SeatbeltCommand; use thinwedge_cli::WindowsCommand; use thinwedge_cli::read_agent_identity_from_stdin; use thinwedge_cli::read_api_key_from_stdin; use thinwedge_cli::run_ardent_cli; +use thinwedge_cli::run_db_sandbox_cli; use thinwedge_cli::run_login_status; use thinwedge_cli::run_login_with_agent_identity; use thinwedge_cli::run_login_with_api_key; @@ -118,6 +120,10 @@ enum Subcommand { /// Manage optional Ardent database sandboxes for finance agents. Ardent(ArdentCli), + /// Configure and preflight DB sandbox providers for finance agents. + #[clap(name = "db-sandbox")] + DbSandbox(DbSandboxCli), + /// Manage external MCP servers for ThinWedge. Mcp(McpCli), @@ -1039,6 +1045,18 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { ); run_ardent_cli(ardent_cli).await?; } + Some(Subcommand::DbSandbox(mut db_sandbox_cli)) => { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "db-sandbox", + )?; + prepend_config_flags( + &mut db_sandbox_cli.config_overrides, + root_config_overrides.clone(), + ); + run_db_sandbox_cli(db_sandbox_cli).await?; + } Some(Subcommand::Completion(completion_cli)) => { reject_remote_mode_for_subcommand( root_remote.as_deref(), diff --git a/thinwedge-rs/config/src/config_toml.rs b/thinwedge-rs/config/src/config_toml.rs index f055cb2af..5ae81dfb8 100644 --- a/thinwedge-rs/config/src/config_toml.rs +++ b/thinwedge-rs/config/src/config_toml.rs @@ -13,6 +13,7 @@ use crate::types::AppsConfigToml; use crate::types::ArdentConfigToml; use crate::types::AuthCredentialsStoreMode; use crate::types::AwsIdentityConfigToml; +use crate::types::DbSandboxConfigToml; use crate::types::FeedbackConfigToml; use crate::types::History; use crate::types::MarketplaceConfig; @@ -403,6 +404,10 @@ pub struct ConfigToml { #[serde(default)] pub db_ops: Option, + /// Generic finance DB sandbox setup. Secrets are referenced by env var name. + #[serde(default)] + pub db_sandbox: Option, + /// Optional Ardent database sandbox integration. #[serde(default)] pub ardent: Option, diff --git a/thinwedge-rs/config/src/types.rs b/thinwedge-rs/config/src/types.rs index 4326c7ce3..da16252af 100644 --- a/thinwedge-rs/config/src/types.rs +++ b/thinwedge-rs/config/src/types.rs @@ -185,6 +185,55 @@ pub struct AwsIdentityConfigToml { pub region: Option, } +/// Source database provider used by the finance DB sandbox preflight path. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DbSandboxProviderToml { + /// Neon Postgres. This is the default first-class provider for ThinWedge's + /// DB sandbox setup flow. + Neon, + /// Generic PostgreSQL-compatible source URL. + Postgresql, + /// AWS RDS Postgres checked through the DB Ops AWS identity. + Rds, + /// Supabase direct Postgres endpoint. + Supabase, + /// PlanetScale source. Preflight currently records intent and skips live checks. + Planetscale, +} + +/// Branch/sandbox backend that receives a verified source DB connection. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DbSandboxBackendToml { + /// Do not configure a branch backend yet; only validate provider readiness. + None, + /// Use Ardent to create isolated branch databases. + Ardent, +} + +/// Generic DB sandbox settings. These are intentionally non-secret; API keys and +/// source URLs are referenced by environment-variable name instead of being +/// stored in config.toml. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct DbSandboxConfigToml { + /// Enables DB sandbox setup for finance/database agents. + pub enabled: Option, + /// Source database provider. Defaults to `neon` in CLI setup flows. + pub provider: Option, + /// Env var containing a source Postgres URL for generic/Postgres-like providers. + pub source_url_env: Option, + /// Env var containing the Neon API key. + pub neon_api_key_env: Option, + /// Env var containing the Neon project id. + pub neon_project_id_env: Option, + /// Optional non-secret Neon project id. API keys must still stay in env/secrets. + pub neon_project_id: Option, + /// Optional branch backend. Defaults to `none` unless the user opts into Ardent. + pub branch_backend: Option, +} + /// Data plane placement for Ardent-managed database sandboxes. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/thinwedge-rs/config/src/types_tests.rs b/thinwedge-rs/config/src/types_tests.rs index 7b2d1b12b..8dd8d06ed 100644 --- a/thinwedge-rs/config/src/types_tests.rs +++ b/thinwedge-rs/config/src/types_tests.rs @@ -101,6 +101,15 @@ fn deserialize_finance_sandbox_config() { role_arn = "arn:aws:iam::123456789012:role/fpna-db-ops" region = "us-west-2" + [db_sandbox] + enabled = true + provider = "neon" + source_url_env = "THINWEDGE_ARDENT_SOURCE_DATABASE_URL" + neon_api_key_env = "THINWEDGE_NEON_API_KEY" + neon_project_id_env = "THINWEDGE_NEON_PROJECT_ID" + neon_project_id = "twilight-lab-63846303" + branch_backend = "ardent" + [ardent] enabled = true cli_path = "ardent" @@ -127,6 +136,20 @@ fn deserialize_finance_sandbox_config() { Some("arn:aws:iam::123456789012:role/fpna-db-ops") ); + let db_sandbox = cfg.db_sandbox.expect("db sandbox config"); + assert_eq!( + db_sandbox.provider, + Some(crate::types::DbSandboxProviderToml::Neon) + ); + assert_eq!( + db_sandbox.branch_backend, + Some(crate::types::DbSandboxBackendToml::Ardent) + ); + assert_eq!( + db_sandbox.neon_project_id.as_deref(), + Some("twilight-lab-63846303") + ); + let ardent = cfg.ardent.expect("ardent config"); assert_eq!(ardent.enabled, Some(true)); assert_eq!(ardent.default_connector.as_deref(), Some("fpna-prod")); diff --git a/thinwedge-rs/core/config.schema.json b/thinwedge-rs/core/config.schema.json index 35e2be069..56d25a169 100644 --- a/thinwedge-rs/core/config.schema.json +++ b/thinwedge-rs/core/config.schema.json @@ -754,6 +754,108 @@ }, "type": "object" }, + "DbSandboxBackendToml": { + "description": "Branch/sandbox backend that receives a verified source DB connection.", + "oneOf": [ + { + "description": "Do not configure a branch backend yet; only validate provider readiness.", + "enum": [ + "none" + ], + "type": "string" + }, + { + "description": "Use Ardent to create isolated branch databases.", + "enum": [ + "ardent" + ], + "type": "string" + } + ] + }, + "DbSandboxConfigToml": { + "additionalProperties": false, + "description": "Generic DB sandbox settings. These are intentionally non-secret; API keys and source URLs are referenced by environment-variable name instead of being stored in config.toml.", + "properties": { + "branch_backend": { + "allOf": [ + { + "$ref": "#/definitions/DbSandboxBackendToml" + } + ], + "description": "Optional branch backend. Defaults to `none` unless the user opts into Ardent." + }, + "enabled": { + "description": "Enables DB sandbox setup for finance/database agents.", + "type": "boolean" + }, + "neon_api_key_env": { + "description": "Env var containing the Neon API key.", + "type": "string" + }, + "neon_project_id": { + "description": "Optional non-secret Neon project id. API keys must still stay in env/secrets.", + "type": "string" + }, + "neon_project_id_env": { + "description": "Env var containing the Neon project id.", + "type": "string" + }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/DbSandboxProviderToml" + } + ], + "description": "Source database provider. Defaults to `neon` in CLI setup flows." + }, + "source_url_env": { + "description": "Env var containing a source Postgres URL for generic/Postgres-like providers.", + "type": "string" + } + }, + "type": "object" + }, + "DbSandboxProviderToml": { + "description": "Source database provider used by the finance DB sandbox preflight path.", + "oneOf": [ + { + "description": "Neon Postgres. This is the default first-class provider for ThinWedge's DB sandbox setup flow.", + "enum": [ + "neon" + ], + "type": "string" + }, + { + "description": "Generic PostgreSQL-compatible source URL.", + "enum": [ + "postgresql" + ], + "type": "string" + }, + { + "description": "AWS RDS Postgres checked through the DB Ops AWS identity.", + "enum": [ + "rds" + ], + "type": "string" + }, + { + "description": "Supabase direct Postgres endpoint.", + "enum": [ + "supabase" + ], + "type": "string" + }, + { + "description": "PlanetScale source. Preflight currently records intent and skips live checks.", + "enum": [ + "planetscale" + ], + "type": "string" + } + ] + }, "ExternalConfigMigrationPrompts": { "additionalProperties": false, "description": "Settings for notices we display to users via the tui and app-server clients (primarily the ThinWedge IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", @@ -3348,6 +3450,15 @@ "default": null, "description": "AWS identity used for database operations such as RDS and Secrets Manager checks." }, + "db_sandbox": { + "allOf": [ + { + "$ref": "#/definitions/DbSandboxConfigToml" + } + ], + "default": null, + "description": "Generic finance DB sandbox setup. Secrets are referenced by env var name." + }, "default_permissions": { "description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.", "type": "string" From acfe4b96560af9eed469288dd67378d32d1d9a4a Mon Sep 17 00:00:00 2001 From: Priyesh Srivastava Date: Sun, 10 May 2026 14:41:52 +0530 Subject: [PATCH 2/2] Keep CircleCI PR status non-release --- .circleci/config.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 38c48ef7b..c5faad84a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,6 +82,17 @@ commands: done < "${env_file}" jobs: + pr-status: + docker: + - image: cimg/base:stable + steps: + - run: + name: CircleCI is release-only + command: | + set -euo pipefail + echo "CircleCI release jobs run only on rust-v* tags." + echo "Pull-request merge gating lives in GitHub Actions." + tag-check: docker: - image: cimg/base:stable @@ -112,13 +123,7 @@ jobs: scripts/probes/check-db-sandbox-readiness.sh --dry-run scripts/probes/check-db-sandbox-readiness.sh --source-provider neon --branch-backend none --include-branch-lifecycle --dry-run scripts/probes/check-db-sandbox-readiness.sh --source-provider planetscale --branch-backend none --dry-run - python3 - <<'PY' - import json - schema = json.load(open("thinwedge-rs/core/config.schema.json", encoding="utf-8")) - props = schema.get("properties", {}) - assert "db_sandbox" in props, "config schema must expose db_sandbox" - assert "ardent" in props, "config schema must keep optional Ardent settings" - PY + python3 -c 'import json; schema = json.load(open("thinwedge-rs/core/config.schema.json", encoding="utf-8")); props = schema.get("properties", {}); assert "db_sandbox" in props, "config schema must expose db_sandbox"; assert "ardent" in props, "config schema must keep optional Ardent settings"' build-linux-x64: machine: @@ -577,6 +582,10 @@ jobs: workflows: release: jobs: + - pr-status: + filters: + tags: + ignore: /.*/ - tag-check: filters: branches: