Skip to content

feat(cli+sdk): Tower Storage from the CLI — credential vending & connection snippets#309

Open
konstantinoscs wants to merge 1 commit into
developfrom
feature/tow-2315-add-cli-commands-and-ux-for-storage-functionality
Open

feat(cli+sdk): Tower Storage from the CLI — credential vending & connection snippets#309
konstantinoscs wants to merge 1 commit into
developfrom
feature/tow-2315-add-cli-commands-and-ux-for-storage-functionality

Conversation

@konstantinoscs

@konstantinoscs konstantinoscs commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Tower-vended catalog credentials to the CLI and Python SDK so users can access Tower-managed Iceberg catalogs without manually configuring PyIceberg credentials.

This introduces a vend-first path for tower.tables(), adds tower catalogs credentials, and improves catalog discovery with storage-focused filters.

Changes

  • Added Python SDK support for Tower-vended Iceberg catalog credentials.
  • Updated tower.tables() so string catalog names use Tower credential vending by default.
  • Added lazy write escalation: reads vend read credentials first, while writes re-vend read-write credentials and reload the table before writing.
  • Kept custom PyIceberg Catalog objects working for tests and advanced/custom usage.
  • Added a temporary tower_credentials=False rollback path for legacy PyIceberg config loading.
  • Added credential caching with refresh before token expiry.
  • Added default-catalog retry handling for first-time provisioning races.
  • Updated Tower context handling to prefer runtime environment values and support logged-in session auth from the local Tower config.
  • Added tower catalogs list --type <type> and tower catalogs list --storage.
  • Added tower catalogs credentials <name> with snippets for:
    • PyIceberg
    • Spark
    • DuckDB
    • dbt
    • DBeaver
  • Made normal credential output avoid printing tokens by default, using TOWER_CATALOG_TOKEN instead.
  • Added --show-token for explicit token printing and --json for raw machine-readable output.

New commands and command variations:

# --- Catalog discovery (new flags on `catalogs list`) ---
tower catalogs list --storage                       # only Tower-managed (tower-catalog) catalogs
tower catalogs list --type tower-catalog            # same, explicit
tower catalogs list --type s3-tables -e production  # filter by any type, scoped to an environment

# --- Vend short-lived credentials + print connection snippets (new subcommand) ---
tower catalogs credentials <name>                          # all snippets, read-only, token redacted
tower catalogs credentials <name> --format pyiceberg       # one tool: pyiceberg | spark | duckdb | dbt | dbeaver | all
tower catalogs credentials <name> --mode read-write        # read-write creds (requires catalog write permission)
tower catalogs credentials <name> -e production            # choose the environment (default: "default")
tower catalogs credentials <name> --show-token             # inline the real token instead of $TOWER_CATALOG_TOKEN
tower --json catalogs credentials <name>                   # raw JSON (includes the token) for scripting

New sdk behavior:

import tower

# String catalog name → Tower-vended credentials (laptop, CI, or in a run)
df = tower.tables("events").load().read()              # read-only creds
tower.tables("events").insert(data)                    # escalates to read-write on write

tower.tables("events", tower_credentials=False)        # opt out → legacy local PyIceberg config
tower.tables("events", catalog=my_pyiceberg_catalog)   # custom Catalog object, unchanged

Some local testing:
List storage catalogs only (catalog type == tower-catalog)
image

Show a catalog:
image

Vend credentials for a catalog:

cargo run -- catalogs credentials default1 --environment default --mode read
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/tower catalogs credentials default1 --environment default --mode read`
✔ Vending catalog credentials... Done!
Catalog: default1
Mode: read
Expires: 2026-06-29T17:24:04.613338+02:00
Token: not printed; snippets read $TOWER_CATALOG_TOKEN
  These credentials are short-lived and intended for ad-hoc development use.

Shell setup
export TOWER_CATALOG_TOKEN="$(tower --json catalogs credentials 'default1' --environment 'default' --mode 'read' | python3 -c 'import json,sys; print(json.load(sys.stdin)["credentials"]["oauth_token"])')"

PyIceberg
import os
from pyiceberg.catalog import load_catalog

catalog = load_catalog(
    "default1",
    type="rest",
    uri="<tower's_very_secret_polaris_instance>",
    warehouse="<fully_qualified_catalog_name>",
    token=os.environ["TOWER_CATALOG_TOKEN"],
)


Spark
import os
from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .config("spark.sql.catalog.default1", "org.apache.iceberg.spark.SparkCatalog")
    .config("spark.sql.catalog.default1.type", "rest")
    .config("spark.sql.catalog.default1.uri", "<tower's_very_secret_polaris_instance>")
    .config("spark.sql.catalog.default1.warehouse", "<fully_qualified_catalog_name>")
    .config("spark.sql.catalog.default1.token", os.environ["TOWER_CATALOG_TOKEN"])
    .getOrCreate()
)


DuckDB
duckdb <<SQL
INSTALL httpfs;
LOAD httpfs;
INSTALL iceberg;
LOAD iceberg;
SET s3_region='eu-central-1';
CREATE OR REPLACE SECRET tower_cat (TYPE iceberg, TOKEN '${TOWER_CATALOG_TOKEN}');
ATTACH '<fully_qualified_catalog_name>' AS "default1" (TYPE iceberg, SECRET tower_cat, ENDPOINT '<tower's_very_secret_polaris_instance>', DEFAULT_REGION 'eu-central-1');
SQL


dbt
type: iceberg
catalog_type: rest
uri: "<tower's_very_secrect_polaris_instance>"
warehouse: "<fully_qualified_catalog_name>"
token: "{{ env_var('TOWER_CATALOG_TOKEN') }}"


DBeaver
Catalog type: Iceberg REST
URI: <tower's_very_secret_polaris_instance>
Warehouse: <fully_qualified_catalog_name>
Authentication: Bearer token
Token: $TOWER_CATALOG_TOKEN

Summary by CodeRabbit

  • New Features

    • Added a new catalogs credentials command to generate short-lived catalog credentials and ready-to-use connection snippets.
    • Added catalog filtering to catalogs list, including a new --type option and a --storage shortcut.
    • Expanded supported snippet formats for tools like DuckDB, dbt, PyIceberg, and Spark.
  • Bug Fixes

    • Improved catalog and table access so write operations automatically switch to the needed access mode.
    • Enhanced session-based settings lookup for more reliable CLI behavior when environment variables are missing.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ WARNING: This PR targets main instead of develop

This PR is targeting main which will trigger a production deployment when merged.

If this is a regular feature/fix PR, please change the base branch to develop.
If this is intentional (e.g., hotfix), you can ignore this warning.

Current base: main
Recommended base: develop

@konstantinoscs konstantinoscs changed the base branch from main to develop June 29, 2026 13:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-binaries.yml:
- Around line 99-104: The Intel macOS wheel job builds and uploads the wheel
without validating it, so add the same smoke-test step used in the other wheel
workflows after the Build wheel step in this job. Use the produced artifact from
the maturin-action output to install the wheel with the matrix Python
interpreter, then run the wheel’s entrypoint/import check so a bad tag or broken
install fails before Upload wheel. Reference the existing Build wheel and Upload
wheel steps in build-binaries.yml and mirror the pattern from the other wheel
jobs.
- Around line 204-206: The checkout step for the containerized build jobs still
leaves persisted credentials available in the workspace, unlike the existing
linux-cross setup. Update the actions/checkout usage in the musllinux and
musllinux-cross job paths to disable credential persistence by adding the same
persist-credentials setting used elsewhere, so the checked-out token is not
exposed to addnab/docker-run-action or uraimo/run-on-arch-action.
- Line 202: The wheel artifact names in the build matrix are colliding across
Python versions, so each upload should be uniquely named per target and
python-version. Update the artifact name used by the upload step in the workflow
to include the matrix python-version suffix, using the existing matrix fields
such as matrix.python-version so Linux, musllinux, and musllinux-cross jobs
preserve distinct wheel sets instead of overwriting each other.

In `@crates/tower-cmd/src/api.rs`:
- Around line 381-405: `vend_catalog_credentials` is passing a sensitive
bearer-token response into `unwrap_api_response`, which can expose
`credentials.oauth_token` in debug logs. Update `vend_catalog_credentials` (and,
if needed, the `unwrap_api_response` call path) to redact or avoid logging the
raw response body for this API before returning the
`VendCatalogCredentialsResponse`, so debug output never prints the token unless
explicitly allowed by the command.

In `@crates/tower-cmd/src/catalogs.rs`:
- Around line 241-247: The token export helper currently hardcodes a POSIX shell
snippet in token_export_command, which won’t work for Windows binaries. Update
token_export_command to detect the target platform and emit a platform-specific
instruction: keep the existing export-based flow for Unix-like shells, but on
Windows return a PowerShell-friendly or manual set/copy command instead of
relying on export, shell quoting, and python3. Use the existing
token_export_command symbol as the place to branch the output.

In `@src/tower/_context.py`:
- Around line 65-83: Normalize empty auth environment values in the
`_context.py` initialization path by treating blank `TOWER_API_KEY` and
`TOWER_JWT` the same as unset before the `_read_session()` fallback. Update the
`tower_api_key`/`tower_jwt` handling in the context-loading logic so `""` does
not count as present, allowing `tower login` session values to be used when env
vars are exported empty. Keep the existing `tower_url` and `tower_run_id` flow
unchanged.

In `@src/tower/_tables.py`:
- Around line 87-97: The current read-write fast path in
_ensure_read_write_table prevents re-entering get_tower_catalog() once
_catalog_mode is already "read-write", which skips the _storage expires_at
refresh for long-lived handles. Update the write/drop/create flow so existing
read-write catalogs still go back through get_tower_catalog() when needed, while
letting the credential cache decide whether to vend new credentials. Keep the
fix centered on _ensure_read_write_table and the related read-write call sites
so refreshed storage credentials are picked up before subsequent operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec0f94ad-4db6-4f0e-bd60-f3b163603e8e

📥 Commits

Reviewing files that changed from the base of the PR and between 83cbf20 and 0594a45.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (247)
  • .github/workflows/build-binaries.yml
  • crates/tower-api/README.md
  • crates/tower-api/src/apis/configuration.rs
  • crates/tower-api/src/apis/default_api.rs
  • crates/tower-api/src/apis/feature_flags_api.rs
  • crates/tower-api/src/models/account.rs
  • crates/tower-api/src/models/acknowledge_alert_response.rs
  • crates/tower-api/src/models/acknowledge_all_alerts_response.rs
  • crates/tower-api/src/models/alert.rs
  • crates/tower-api/src/models/api_key.rs
  • crates/tower-api/src/models/api_key_owner.rs
  • crates/tower-api/src/models/app.rs
  • crates/tower-api/src/models/app_statistics.rs
  • crates/tower-api/src/models/app_summary.rs
  • crates/tower-api/src/models/app_version.rs
  • crates/tower-api/src/models/authentication_context.rs
  • crates/tower-api/src/models/batch_schedule_params.rs
  • crates/tower-api/src/models/batch_schedule_response.rs
  • crates/tower-api/src/models/cancel_run_response.rs
  • crates/tower-api/src/models/catalog.rs
  • crates/tower-api/src/models/catalog_credentials.rs
  • crates/tower-api/src/models/catalog_property.rs
  • crates/tower-api/src/models/claim_device_login_ticket_params.rs
  • crates/tower-api/src/models/claim_device_login_ticket_response.rs
  • crates/tower-api/src/models/create_account_params.rs
  • crates/tower-api/src/models/create_account_params_flags_struct.rs
  • crates/tower-api/src/models/create_account_response.rs
  • crates/tower-api/src/models/create_api_key_params.rs
  • crates/tower-api/src/models/create_api_key_response.rs
  • crates/tower-api/src/models/create_app_params.rs
  • crates/tower-api/src/models/create_app_response.rs
  • crates/tower-api/src/models/create_catalog_params.rs
  • crates/tower-api/src/models/create_catalog_response.rs
  • crates/tower-api/src/models/create_device_login_ticket_response.rs
  • crates/tower-api/src/models/create_environment_params.rs
  • crates/tower-api/src/models/create_environment_response.rs
  • crates/tower-api/src/models/create_guest_params.rs
  • crates/tower-api/src/models/create_guest_response.rs
  • crates/tower-api/src/models/create_sandbox_secrets_params.rs
  • crates/tower-api/src/models/create_sandbox_secrets_response.rs
  • crates/tower-api/src/models/create_schedule_params.rs
  • crates/tower-api/src/models/create_schedule_response.rs
  • crates/tower-api/src/models/create_secret_params.rs
  • crates/tower-api/src/models/create_secret_response.rs
  • crates/tower-api/src/models/create_service_account_api_key_params.rs
  • crates/tower-api/src/models/create_service_account_api_key_response.rs
  • crates/tower-api/src/models/create_service_account_params.rs
  • crates/tower-api/src/models/create_service_account_response.rs
  • crates/tower-api/src/models/create_session_params.rs
  • crates/tower-api/src/models/create_session_response.rs
  • crates/tower-api/src/models/create_team_params.rs
  • crates/tower-api/src/models/create_team_response.rs
  • crates/tower-api/src/models/create_webhook_params.rs
  • crates/tower-api/src/models/create_webhook_response.rs
  • crates/tower-api/src/models/delete_api_key_params.rs
  • crates/tower-api/src/models/delete_api_key_response.rs
  • crates/tower-api/src/models/delete_app_response.rs
  • crates/tower-api/src/models/delete_catalog_response.rs
  • crates/tower-api/src/models/delete_environment_response.rs
  • crates/tower-api/src/models/delete_guest_output_body.rs
  • crates/tower-api/src/models/delete_schedule_params.rs
  • crates/tower-api/src/models/delete_schedule_response.rs
  • crates/tower-api/src/models/delete_secret_response.rs
  • crates/tower-api/src/models/delete_service_account_api_key_params.rs
  • crates/tower-api/src/models/delete_session_params.rs
  • crates/tower-api/src/models/delete_session_response.rs
  • crates/tower-api/src/models/delete_team_invitation_params.rs
  • crates/tower-api/src/models/delete_team_invitation_response.rs
  • crates/tower-api/src/models/delete_team_params.rs
  • crates/tower-api/src/models/delete_team_response.rs
  • crates/tower-api/src/models/delete_webhook_response.rs
  • crates/tower-api/src/models/deploy_app_request.rs
  • crates/tower-api/src/models/deploy_app_response.rs
  • crates/tower-api/src/models/describe_account_body.rs
  • crates/tower-api/src/models/describe_app_response.rs
  • crates/tower-api/src/models/describe_app_version_response.rs
  • crates/tower-api/src/models/describe_authentication_context_body.rs
  • crates/tower-api/src/models/describe_catalog_response.rs
  • crates/tower-api/src/models/describe_device_login_session_response.rs
  • crates/tower-api/src/models/describe_email_preferences_body.rs
  • crates/tower-api/src/models/describe_environment_response.rs
  • crates/tower-api/src/models/describe_plan_response.rs
  • crates/tower-api/src/models/describe_run_graph_response.rs
  • crates/tower-api/src/models/describe_run_links.rs
  • crates/tower-api/src/models/describe_run_logs_response.rs
  • crates/tower-api/src/models/describe_run_response.rs
  • crates/tower-api/src/models/describe_secrets_key_response.rs
  • crates/tower-api/src/models/describe_service_account_response.rs
  • crates/tower-api/src/models/describe_session_response.rs
  • crates/tower-api/src/models/describe_team_response.rs
  • crates/tower-api/src/models/describe_webhook_response.rs
  • crates/tower-api/src/models/describe_whoami_response.rs
  • crates/tower-api/src/models/email_subscriptions.rs
  • crates/tower-api/src/models/encrypted_catalog_property.rs
  • crates/tower-api/src/models/environment.rs
  • crates/tower-api/src/models/error_detail.rs
  • crates/tower-api/src/models/error_model.rs
  • crates/tower-api/src/models/event_alert.rs
  • crates/tower-api/src/models/event_error.rs
  • crates/tower-api/src/models/event_log.rs
  • crates/tower-api/src/models/event_shouldertap.rs
  • crates/tower-api/src/models/event_warning.rs
  • crates/tower-api/src/models/export_catalogs_params.rs
  • crates/tower-api/src/models/export_catalogs_response.rs
  • crates/tower-api/src/models/export_secrets_params.rs
  • crates/tower-api/src/models/export_secrets_response.rs
  • crates/tower-api/src/models/exported_catalog.rs
  • crates/tower-api/src/models/exported_catalog_property.rs
  • crates/tower-api/src/models/exported_secret.rs
  • crates/tower-api/src/models/feature.rs
  • crates/tower-api/src/models/featurebase_identity.rs
  • crates/tower-api/src/models/generate_app_statistics_response.rs
  • crates/tower-api/src/models/generate_organization_usage_time_series_response.rs
  • crates/tower-api/src/models/generate_run_statistics_response.rs
  • crates/tower-api/src/models/generate_runner_credentials_response.rs
  • crates/tower-api/src/models/get_feature_flag_response_body.rs
  • crates/tower-api/src/models/guest.rs
  • crates/tower-api/src/models/invite_team_member_params.rs
  • crates/tower-api/src/models/invite_team_member_response.rs
  • crates/tower-api/src/models/leave_team_response.rs
  • crates/tower-api/src/models/list_alerts_response.rs
  • crates/tower-api/src/models/list_api_keys_response.rs
  • crates/tower-api/src/models/list_app_environments_response.rs
  • crates/tower-api/src/models/list_app_versions_response.rs
  • crates/tower-api/src/models/list_apps_response.rs
  • crates/tower-api/src/models/list_catalogs_response.rs
  • crates/tower-api/src/models/list_environments_response.rs
  • crates/tower-api/src/models/list_guests_response.rs
  • crates/tower-api/src/models/list_my_team_invitations_response.rs
  • crates/tower-api/src/models/list_runners_response.rs
  • crates/tower-api/src/models/list_runs_response.rs
  • crates/tower-api/src/models/list_schedules_response.rs
  • crates/tower-api/src/models/list_secret_environments_response.rs
  • crates/tower-api/src/models/list_secrets_response.rs
  • crates/tower-api/src/models/list_service_account_api_keys_response.rs
  • crates/tower-api/src/models/list_service_accounts_response.rs
  • crates/tower-api/src/models/list_team_invitations_response.rs
  • crates/tower-api/src/models/list_team_members_response.rs
  • crates/tower-api/src/models/list_teams_response.rs
  • crates/tower-api/src/models/list_webhooks_response.rs
  • crates/tower-api/src/models/organization.rs
  • crates/tower-api/src/models/organization_usage.rs
  • crates/tower-api/src/models/pagination.rs
  • crates/tower-api/src/models/parameter.rs
  • crates/tower-api/src/models/plan.rs
  • crates/tower-api/src/models/refresh_session_params.rs
  • crates/tower-api/src/models/refresh_session_response.rs
  • crates/tower-api/src/models/regenerate_guest_login_url_params.rs
  • crates/tower-api/src/models/regenerate_guest_login_url_response.rs
  • crates/tower-api/src/models/remove_team_member_params.rs
  • crates/tower-api/src/models/remove_team_member_response.rs
  • crates/tower-api/src/models/resend_team_invitation_params.rs
  • crates/tower-api/src/models/resend_team_invitation_response.rs
  • crates/tower-api/src/models/run.rs
  • crates/tower-api/src/models/run_app_initiator_data.rs
  • crates/tower-api/src/models/run_app_params.rs
  • crates/tower-api/src/models/run_app_response.rs
  • crates/tower-api/src/models/run_attempt.rs
  • crates/tower-api/src/models/run_creator.rs
  • crates/tower-api/src/models/run_failure_alert.rs
  • crates/tower-api/src/models/run_graph_node.rs
  • crates/tower-api/src/models/run_graph_run_id.rs
  • crates/tower-api/src/models/run_initiator.rs
  • crates/tower-api/src/models/run_initiator_details.rs
  • crates/tower-api/src/models/run_log_line.rs
  • crates/tower-api/src/models/run_parameter.rs
  • crates/tower-api/src/models/run_results.rs
  • crates/tower-api/src/models/run_retry_policy.rs
  • crates/tower-api/src/models/run_run_initiator_details.rs
  • crates/tower-api/src/models/run_statistics.rs
  • crates/tower-api/src/models/run_timeseries_point.rs
  • crates/tower-api/src/models/runner.rs
  • crates/tower-api/src/models/runner_credentials.rs
  • crates/tower-api/src/models/schedule.rs
  • crates/tower-api/src/models/schedule_owner.rs
  • crates/tower-api/src/models/schedule_run_initiator_details.rs
  • crates/tower-api/src/models/search_runs_response.rs
  • crates/tower-api/src/models/secret.rs
  • crates/tower-api/src/models/server_sent_events_inner.rs
  • crates/tower-api/src/models/server_sent_events_inner_1.rs
  • crates/tower-api/src/models/server_sent_events_inner_2.rs
  • crates/tower-api/src/models/service_account.rs
  • crates/tower-api/src/models/service_account_creator.rs
  • crates/tower-api/src/models/session.rs
  • crates/tower-api/src/models/shoulder_tap.rs
  • crates/tower-api/src/models/sse_warning.rs
  • crates/tower-api/src/models/statistics_settings.rs
  • crates/tower-api/src/models/team.rs
  • crates/tower-api/src/models/team_invitation.rs
  • crates/tower-api/src/models/team_membership.rs
  • crates/tower-api/src/models/test_webhook_response.rs
  • crates/tower-api/src/models/token.rs
  • crates/tower-api/src/models/update_account_params.rs
  • crates/tower-api/src/models/update_account_response.rs
  • crates/tower-api/src/models/update_app_environment_params.rs
  • crates/tower-api/src/models/update_app_environment_response.rs
  • crates/tower-api/src/models/update_app_params.rs
  • crates/tower-api/src/models/update_app_response.rs
  • crates/tower-api/src/models/update_catalog_params.rs
  • crates/tower-api/src/models/update_catalog_response.rs
  • crates/tower-api/src/models/update_email_preferences_body.rs
  • crates/tower-api/src/models/update_environment_params.rs
  • crates/tower-api/src/models/update_environment_response.rs
  • crates/tower-api/src/models/update_my_team_invitation_params.rs
  • crates/tower-api/src/models/update_my_team_invitation_response.rs
  • crates/tower-api/src/models/update_organization_params.rs
  • crates/tower-api/src/models/update_organization_response.rs
  • crates/tower-api/src/models/update_plan_params.rs
  • crates/tower-api/src/models/update_plan_response.rs
  • crates/tower-api/src/models/update_schedule_params.rs
  • crates/tower-api/src/models/update_schedule_response.rs
  • crates/tower-api/src/models/update_secret_params.rs
  • crates/tower-api/src/models/update_secret_response.rs
  • crates/tower-api/src/models/update_service_account_params.rs
  • crates/tower-api/src/models/update_service_account_response.rs
  • crates/tower-api/src/models/update_team_member_params.rs
  • crates/tower-api/src/models/update_team_member_response.rs
  • crates/tower-api/src/models/update_team_params.rs
  • crates/tower-api/src/models/update_team_response.rs
  • crates/tower-api/src/models/update_user_params.rs
  • crates/tower-api/src/models/update_user_response.rs
  • crates/tower-api/src/models/update_webhook_params.rs
  • crates/tower-api/src/models/update_webhook_response.rs
  • crates/tower-api/src/models/usage_limit.rs
  • crates/tower-api/src/models/usage_metric_time_series_point.rs
  • crates/tower-api/src/models/user.rs
  • crates/tower-api/src/models/vend_catalog_credentials_body.rs
  • crates/tower-api/src/models/vend_catalog_credentials_response.rs
  • crates/tower-api/src/models/webhook.rs
  • crates/tower-cmd/src/api.rs
  • crates/tower-cmd/src/catalogs.rs
  • crates/tower-cmd/src/lib.rs
  • crates/tower-cmd/src/mcp.rs
  • crates/tower-cmd/src/templates/dbeaver.txt.tmpl
  • crates/tower-cmd/src/templates/dbt.yml.tmpl
  • crates/tower-cmd/src/templates/duckdb.sql.tmpl
  • crates/tower-cmd/src/templates/pyiceberg.py.tmpl
  • crates/tower-cmd/src/templates/spark.py.tmpl
  • pyproject.toml
  • src/tower/_context.py
  • src/tower/_storage.py
  • src/tower/_tables.py
  • src/tower/tower_api_client/api/default/generate_organization_usage_time_series.py
  • src/tower/tower_api_client/api/default/vend_catalog_credentials.py
  • src/tower/tower_api_client/models/vend_catalog_credentials_body.py
  • tests/tower/test_storage.py
  • tests/tower/test_tables.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-binaries.yml:
- Around line 99-104: The Intel macOS wheel job builds and uploads the wheel
without validating it, so add the same smoke-test step used in the other wheel
workflows after the Build wheel step in this job. Use the produced artifact from
the maturin-action output to install the wheel with the matrix Python
interpreter, then run the wheel’s entrypoint/import check so a bad tag or broken
install fails before Upload wheel. Reference the existing Build wheel and Upload
wheel steps in build-binaries.yml and mirror the pattern from the other wheel
jobs.
- Around line 204-206: The checkout step for the containerized build jobs still
leaves persisted credentials available in the workspace, unlike the existing
linux-cross setup. Update the actions/checkout usage in the musllinux and
musllinux-cross job paths to disable credential persistence by adding the same
persist-credentials setting used elsewhere, so the checked-out token is not
exposed to addnab/docker-run-action or uraimo/run-on-arch-action.
- Line 202: The wheel artifact names in the build matrix are colliding across
Python versions, so each upload should be uniquely named per target and
python-version. Update the artifact name used by the upload step in the workflow
to include the matrix python-version suffix, using the existing matrix fields
such as matrix.python-version so Linux, musllinux, and musllinux-cross jobs
preserve distinct wheel sets instead of overwriting each other.

In `@crates/tower-cmd/src/api.rs`:
- Around line 381-405: `vend_catalog_credentials` is passing a sensitive
bearer-token response into `unwrap_api_response`, which can expose
`credentials.oauth_token` in debug logs. Update `vend_catalog_credentials` (and,
if needed, the `unwrap_api_response` call path) to redact or avoid logging the
raw response body for this API before returning the
`VendCatalogCredentialsResponse`, so debug output never prints the token unless
explicitly allowed by the command.

In `@crates/tower-cmd/src/catalogs.rs`:
- Around line 241-247: The token export helper currently hardcodes a POSIX shell
snippet in token_export_command, which won’t work for Windows binaries. Update
token_export_command to detect the target platform and emit a platform-specific
instruction: keep the existing export-based flow for Unix-like shells, but on
Windows return a PowerShell-friendly or manual set/copy command instead of
relying on export, shell quoting, and python3. Use the existing
token_export_command symbol as the place to branch the output.

In `@src/tower/_context.py`:
- Around line 65-83: Normalize empty auth environment values in the
`_context.py` initialization path by treating blank `TOWER_API_KEY` and
`TOWER_JWT` the same as unset before the `_read_session()` fallback. Update the
`tower_api_key`/`tower_jwt` handling in the context-loading logic so `""` does
not count as present, allowing `tower login` session values to be used when env
vars are exported empty. Keep the existing `tower_url` and `tower_run_id` flow
unchanged.

In `@src/tower/_tables.py`:
- Around line 87-97: The current read-write fast path in
_ensure_read_write_table prevents re-entering get_tower_catalog() once
_catalog_mode is already "read-write", which skips the _storage expires_at
refresh for long-lived handles. Update the write/drop/create flow so existing
read-write catalogs still go back through get_tower_catalog() when needed, while
letting the credential cache decide whether to vend new credentials. Keep the
fix centered on _ensure_read_write_table and the related read-write call sites
so refreshed storage credentials are picked up before subsequent operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec0f94ad-4db6-4f0e-bd60-f3b163603e8e

📥 Commits

Reviewing files that changed from the base of the PR and between 83cbf20 and 0594a45.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (247)
  • .github/workflows/build-binaries.yml
  • crates/tower-api/README.md
  • crates/tower-api/src/apis/configuration.rs
  • crates/tower-api/src/apis/default_api.rs
  • crates/tower-api/src/apis/feature_flags_api.rs
  • crates/tower-api/src/models/account.rs
  • crates/tower-api/src/models/acknowledge_alert_response.rs
  • crates/tower-api/src/models/acknowledge_all_alerts_response.rs
  • crates/tower-api/src/models/alert.rs
  • crates/tower-api/src/models/api_key.rs
  • crates/tower-api/src/models/api_key_owner.rs
  • crates/tower-api/src/models/app.rs
  • crates/tower-api/src/models/app_statistics.rs
  • crates/tower-api/src/models/app_summary.rs
  • crates/tower-api/src/models/app_version.rs
  • crates/tower-api/src/models/authentication_context.rs
  • crates/tower-api/src/models/batch_schedule_params.rs
  • crates/tower-api/src/models/batch_schedule_response.rs
  • crates/tower-api/src/models/cancel_run_response.rs
  • crates/tower-api/src/models/catalog.rs
  • crates/tower-api/src/models/catalog_credentials.rs
  • crates/tower-api/src/models/catalog_property.rs
  • crates/tower-api/src/models/claim_device_login_ticket_params.rs
  • crates/tower-api/src/models/claim_device_login_ticket_response.rs
  • crates/tower-api/src/models/create_account_params.rs
  • crates/tower-api/src/models/create_account_params_flags_struct.rs
  • crates/tower-api/src/models/create_account_response.rs
  • crates/tower-api/src/models/create_api_key_params.rs
  • crates/tower-api/src/models/create_api_key_response.rs
  • crates/tower-api/src/models/create_app_params.rs
  • crates/tower-api/src/models/create_app_response.rs
  • crates/tower-api/src/models/create_catalog_params.rs
  • crates/tower-api/src/models/create_catalog_response.rs
  • crates/tower-api/src/models/create_device_login_ticket_response.rs
  • crates/tower-api/src/models/create_environment_params.rs
  • crates/tower-api/src/models/create_environment_response.rs
  • crates/tower-api/src/models/create_guest_params.rs
  • crates/tower-api/src/models/create_guest_response.rs
  • crates/tower-api/src/models/create_sandbox_secrets_params.rs
  • crates/tower-api/src/models/create_sandbox_secrets_response.rs
  • crates/tower-api/src/models/create_schedule_params.rs
  • crates/tower-api/src/models/create_schedule_response.rs
  • crates/tower-api/src/models/create_secret_params.rs
  • crates/tower-api/src/models/create_secret_response.rs
  • crates/tower-api/src/models/create_service_account_api_key_params.rs
  • crates/tower-api/src/models/create_service_account_api_key_response.rs
  • crates/tower-api/src/models/create_service_account_params.rs
  • crates/tower-api/src/models/create_service_account_response.rs
  • crates/tower-api/src/models/create_session_params.rs
  • crates/tower-api/src/models/create_session_response.rs
  • crates/tower-api/src/models/create_team_params.rs
  • crates/tower-api/src/models/create_team_response.rs
  • crates/tower-api/src/models/create_webhook_params.rs
  • crates/tower-api/src/models/create_webhook_response.rs
  • crates/tower-api/src/models/delete_api_key_params.rs
  • crates/tower-api/src/models/delete_api_key_response.rs
  • crates/tower-api/src/models/delete_app_response.rs
  • crates/tower-api/src/models/delete_catalog_response.rs
  • crates/tower-api/src/models/delete_environment_response.rs
  • crates/tower-api/src/models/delete_guest_output_body.rs
  • crates/tower-api/src/models/delete_schedule_params.rs
  • crates/tower-api/src/models/delete_schedule_response.rs
  • crates/tower-api/src/models/delete_secret_response.rs
  • crates/tower-api/src/models/delete_service_account_api_key_params.rs
  • crates/tower-api/src/models/delete_session_params.rs
  • crates/tower-api/src/models/delete_session_response.rs
  • crates/tower-api/src/models/delete_team_invitation_params.rs
  • crates/tower-api/src/models/delete_team_invitation_response.rs
  • crates/tower-api/src/models/delete_team_params.rs
  • crates/tower-api/src/models/delete_team_response.rs
  • crates/tower-api/src/models/delete_webhook_response.rs
  • crates/tower-api/src/models/deploy_app_request.rs
  • crates/tower-api/src/models/deploy_app_response.rs
  • crates/tower-api/src/models/describe_account_body.rs
  • crates/tower-api/src/models/describe_app_response.rs
  • crates/tower-api/src/models/describe_app_version_response.rs
  • crates/tower-api/src/models/describe_authentication_context_body.rs
  • crates/tower-api/src/models/describe_catalog_response.rs
  • crates/tower-api/src/models/describe_device_login_session_response.rs
  • crates/tower-api/src/models/describe_email_preferences_body.rs
  • crates/tower-api/src/models/describe_environment_response.rs
  • crates/tower-api/src/models/describe_plan_response.rs
  • crates/tower-api/src/models/describe_run_graph_response.rs
  • crates/tower-api/src/models/describe_run_links.rs
  • crates/tower-api/src/models/describe_run_logs_response.rs
  • crates/tower-api/src/models/describe_run_response.rs
  • crates/tower-api/src/models/describe_secrets_key_response.rs
  • crates/tower-api/src/models/describe_service_account_response.rs
  • crates/tower-api/src/models/describe_session_response.rs
  • crates/tower-api/src/models/describe_team_response.rs
  • crates/tower-api/src/models/describe_webhook_response.rs
  • crates/tower-api/src/models/describe_whoami_response.rs
  • crates/tower-api/src/models/email_subscriptions.rs
  • crates/tower-api/src/models/encrypted_catalog_property.rs
  • crates/tower-api/src/models/environment.rs
  • crates/tower-api/src/models/error_detail.rs
  • crates/tower-api/src/models/error_model.rs
  • crates/tower-api/src/models/event_alert.rs
  • crates/tower-api/src/models/event_error.rs
  • crates/tower-api/src/models/event_log.rs
  • crates/tower-api/src/models/event_shouldertap.rs
  • crates/tower-api/src/models/event_warning.rs
  • crates/tower-api/src/models/export_catalogs_params.rs
  • crates/tower-api/src/models/export_catalogs_response.rs
  • crates/tower-api/src/models/export_secrets_params.rs
  • crates/tower-api/src/models/export_secrets_response.rs
  • crates/tower-api/src/models/exported_catalog.rs
  • crates/tower-api/src/models/exported_catalog_property.rs
  • crates/tower-api/src/models/exported_secret.rs
  • crates/tower-api/src/models/feature.rs
  • crates/tower-api/src/models/featurebase_identity.rs
  • crates/tower-api/src/models/generate_app_statistics_response.rs
  • crates/tower-api/src/models/generate_organization_usage_time_series_response.rs
  • crates/tower-api/src/models/generate_run_statistics_response.rs
  • crates/tower-api/src/models/generate_runner_credentials_response.rs
  • crates/tower-api/src/models/get_feature_flag_response_body.rs
  • crates/tower-api/src/models/guest.rs
  • crates/tower-api/src/models/invite_team_member_params.rs
  • crates/tower-api/src/models/invite_team_member_response.rs
  • crates/tower-api/src/models/leave_team_response.rs
  • crates/tower-api/src/models/list_alerts_response.rs
  • crates/tower-api/src/models/list_api_keys_response.rs
  • crates/tower-api/src/models/list_app_environments_response.rs
  • crates/tower-api/src/models/list_app_versions_response.rs
  • crates/tower-api/src/models/list_apps_response.rs
  • crates/tower-api/src/models/list_catalogs_response.rs
  • crates/tower-api/src/models/list_environments_response.rs
  • crates/tower-api/src/models/list_guests_response.rs
  • crates/tower-api/src/models/list_my_team_invitations_response.rs
  • crates/tower-api/src/models/list_runners_response.rs
  • crates/tower-api/src/models/list_runs_response.rs
  • crates/tower-api/src/models/list_schedules_response.rs
  • crates/tower-api/src/models/list_secret_environments_response.rs
  • crates/tower-api/src/models/list_secrets_response.rs
  • crates/tower-api/src/models/list_service_account_api_keys_response.rs
  • crates/tower-api/src/models/list_service_accounts_response.rs
  • crates/tower-api/src/models/list_team_invitations_response.rs
  • crates/tower-api/src/models/list_team_members_response.rs
  • crates/tower-api/src/models/list_teams_response.rs
  • crates/tower-api/src/models/list_webhooks_response.rs
  • crates/tower-api/src/models/organization.rs
  • crates/tower-api/src/models/organization_usage.rs
  • crates/tower-api/src/models/pagination.rs
  • crates/tower-api/src/models/parameter.rs
  • crates/tower-api/src/models/plan.rs
  • crates/tower-api/src/models/refresh_session_params.rs
  • crates/tower-api/src/models/refresh_session_response.rs
  • crates/tower-api/src/models/regenerate_guest_login_url_params.rs
  • crates/tower-api/src/models/regenerate_guest_login_url_response.rs
  • crates/tower-api/src/models/remove_team_member_params.rs
  • crates/tower-api/src/models/remove_team_member_response.rs
  • crates/tower-api/src/models/resend_team_invitation_params.rs
  • crates/tower-api/src/models/resend_team_invitation_response.rs
  • crates/tower-api/src/models/run.rs
  • crates/tower-api/src/models/run_app_initiator_data.rs
  • crates/tower-api/src/models/run_app_params.rs
  • crates/tower-api/src/models/run_app_response.rs
  • crates/tower-api/src/models/run_attempt.rs
  • crates/tower-api/src/models/run_creator.rs
  • crates/tower-api/src/models/run_failure_alert.rs
  • crates/tower-api/src/models/run_graph_node.rs
  • crates/tower-api/src/models/run_graph_run_id.rs
  • crates/tower-api/src/models/run_initiator.rs
  • crates/tower-api/src/models/run_initiator_details.rs
  • crates/tower-api/src/models/run_log_line.rs
  • crates/tower-api/src/models/run_parameter.rs
  • crates/tower-api/src/models/run_results.rs
  • crates/tower-api/src/models/run_retry_policy.rs
  • crates/tower-api/src/models/run_run_initiator_details.rs
  • crates/tower-api/src/models/run_statistics.rs
  • crates/tower-api/src/models/run_timeseries_point.rs
  • crates/tower-api/src/models/runner.rs
  • crates/tower-api/src/models/runner_credentials.rs
  • crates/tower-api/src/models/schedule.rs
  • crates/tower-api/src/models/schedule_owner.rs
  • crates/tower-api/src/models/schedule_run_initiator_details.rs
  • crates/tower-api/src/models/search_runs_response.rs
  • crates/tower-api/src/models/secret.rs
  • crates/tower-api/src/models/server_sent_events_inner.rs
  • crates/tower-api/src/models/server_sent_events_inner_1.rs
  • crates/tower-api/src/models/server_sent_events_inner_2.rs
  • crates/tower-api/src/models/service_account.rs
  • crates/tower-api/src/models/service_account_creator.rs
  • crates/tower-api/src/models/session.rs
  • crates/tower-api/src/models/shoulder_tap.rs
  • crates/tower-api/src/models/sse_warning.rs
  • crates/tower-api/src/models/statistics_settings.rs
  • crates/tower-api/src/models/team.rs
  • crates/tower-api/src/models/team_invitation.rs
  • crates/tower-api/src/models/team_membership.rs
  • crates/tower-api/src/models/test_webhook_response.rs
  • crates/tower-api/src/models/token.rs
  • crates/tower-api/src/models/update_account_params.rs
  • crates/tower-api/src/models/update_account_response.rs
  • crates/tower-api/src/models/update_app_environment_params.rs
  • crates/tower-api/src/models/update_app_environment_response.rs
  • crates/tower-api/src/models/update_app_params.rs
  • crates/tower-api/src/models/update_app_response.rs
  • crates/tower-api/src/models/update_catalog_params.rs
  • crates/tower-api/src/models/update_catalog_response.rs
  • crates/tower-api/src/models/update_email_preferences_body.rs
  • crates/tower-api/src/models/update_environment_params.rs
  • crates/tower-api/src/models/update_environment_response.rs
  • crates/tower-api/src/models/update_my_team_invitation_params.rs
  • crates/tower-api/src/models/update_my_team_invitation_response.rs
  • crates/tower-api/src/models/update_organization_params.rs
  • crates/tower-api/src/models/update_organization_response.rs
  • crates/tower-api/src/models/update_plan_params.rs
  • crates/tower-api/src/models/update_plan_response.rs
  • crates/tower-api/src/models/update_schedule_params.rs
  • crates/tower-api/src/models/update_schedule_response.rs
  • crates/tower-api/src/models/update_secret_params.rs
  • crates/tower-api/src/models/update_secret_response.rs
  • crates/tower-api/src/models/update_service_account_params.rs
  • crates/tower-api/src/models/update_service_account_response.rs
  • crates/tower-api/src/models/update_team_member_params.rs
  • crates/tower-api/src/models/update_team_member_response.rs
  • crates/tower-api/src/models/update_team_params.rs
  • crates/tower-api/src/models/update_team_response.rs
  • crates/tower-api/src/models/update_user_params.rs
  • crates/tower-api/src/models/update_user_response.rs
  • crates/tower-api/src/models/update_webhook_params.rs
  • crates/tower-api/src/models/update_webhook_response.rs
  • crates/tower-api/src/models/usage_limit.rs
  • crates/tower-api/src/models/usage_metric_time_series_point.rs
  • crates/tower-api/src/models/user.rs
  • crates/tower-api/src/models/vend_catalog_credentials_body.rs
  • crates/tower-api/src/models/vend_catalog_credentials_response.rs
  • crates/tower-api/src/models/webhook.rs
  • crates/tower-cmd/src/api.rs
  • crates/tower-cmd/src/catalogs.rs
  • crates/tower-cmd/src/lib.rs
  • crates/tower-cmd/src/mcp.rs
  • crates/tower-cmd/src/templates/dbeaver.txt.tmpl
  • crates/tower-cmd/src/templates/dbt.yml.tmpl
  • crates/tower-cmd/src/templates/duckdb.sql.tmpl
  • crates/tower-cmd/src/templates/pyiceberg.py.tmpl
  • crates/tower-cmd/src/templates/spark.py.tmpl
  • pyproject.toml
  • src/tower/_context.py
  • src/tower/_storage.py
  • src/tower/_tables.py
  • src/tower/tower_api_client/api/default/generate_organization_usage_time_series.py
  • src/tower/tower_api_client/api/default/vend_catalog_credentials.py
  • src/tower/tower_api_client/models/vend_catalog_credentials_body.py
  • tests/tower/test_storage.py
  • tests/tower/test_tables.py
🛑 Comments failed to post (7)
.github/workflows/build-binaries.yml (3)

99-104: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add the same wheel smoke test on Intel macOS.

This leg now varies the interpreter with -i python${{ matrix.python-version }}, but unlike the other wheel jobs it never installs the produced wheel. A bad wheel tag or broken entrypoint on macOS x86_64 would still upload successfully.

Suggested change
       - name: Build wheel
         uses: PyO3/maturin-action@v1.50.1
         with:
           target: x86_64
           args: --release --locked --out dist -i python${{ matrix.python-version }}
+      - name: Test wheel
+        run: |
+          pip install dist/*.whl --force-reinstall
+          ${{ env.EXECUTABLE_NAME }} --help
+          python -m ${{ env.MODULE_NAME }} --help
       - name: Upload wheel
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      - name: Build wheel
        uses: PyO3/maturin-action@v1.50.1
        with:
          target: x86_64
          args: --release --locked --out dist -i python${{ matrix.python-version }}
      - name: Test wheel
        run: |
          pip install dist/*.whl --force-reinstall
          ${{ env.EXECUTABLE_NAME }} --help
          python -m ${{ env.MODULE_NAME }} --help
      - name: Upload wheel
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-binaries.yml around lines 99 - 104, The Intel macOS
wheel job builds and uploads the wheel without validating it, so add the same
smoke-test step used in the other wheel workflows after the Build wheel step in
this job. Use the produced artifact from the maturin-action output to install
the wheel with the matrix Python interpreter, then run the wheel’s
entrypoint/import check so a bad tag or broken install fails before Upload
wheel. Reference the existing Build wheel and Upload wheel steps in
build-binaries.yml and mirror the pattern from the other wheel jobs.

202-202: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the workflow around the referenced lines
nl -ba .github/workflows/build-binaries.yml | sed -n '180,230p;290,325p;335,365p'

# Find every upload-artifact name in this workflow
rg -n 'upload-artifact|name:\s*wheels-' .github/workflows/build-binaries.yml

Repository: tower/tower-cli

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant sections with line numbers using sed only
sed -n '180,230p' .github/workflows/build-binaries.yml | cat -n
printf '\n---\n'
sed -n '290,325p' .github/workflows/build-binaries.yml | cat -n
printf '\n---\n'
sed -n '335,365p' .github/workflows/build-binaries.yml | cat -n

printf '\n=== matches ===\n'
rg -n 'upload-artifact|name:\s*wheels-' .github/workflows/build-binaries.yml

Repository: tower/tower-cli

Length of output: 6104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '220,250p' .github/workflows/build-binaries.yml | cat -n
printf '\n---\n'
sed -n '325,345p' .github/workflows/build-binaries.yml | cat -n
printf '\n---\n'
sed -n '370,385p' .github/workflows/build-binaries.yml | cat -n

Repository: tower/tower-cli

Length of output: 3087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

awk 'NR>=175 && NR<=210 {printf "%4d %s\n", NR, $0}' .github/workflows/build-binaries.yml
printf '\n---\n'
awk 'NR>=300 && NR<=315 {printf "%4d %s\n", NR, $0}' .github/workflows/build-binaries.yml
printf '\n---\n'
awk 'NR>=340 && NR<=355 {printf "%4d %s\n", NR, $0}' .github/workflows/build-binaries.yml
printf '\n---\n'
awk 'NR>=372 && NR<=385 {printf "%4d %s\n", NR, $0}' .github/workflows/build-binaries.yml

Repository: tower/tower-cli

Length of output: 3120


🌐 Web query:

actions/upload-artifact v6 same artifact name matrix overwrite behavior duplicate uploads

💡 Result:

In actions/upload-artifact v6, the default behavior when attempting to upload an artifact with a name that already exists in the same workflow run is to overwrite the existing artifact [1][2]. This represents a change in behavior from earlier versions (such as v4 and v5), where uploading to the same artifact name across multiple jobs or matrix runs would cause the action to fail with a conflict error [1][3][4]. Key details regarding this behavior include: 1. Overwrite Mechanism: When you use the same artifact name, v6 allows the upload to proceed, effectively replacing the previous artifact with the new one [1][2]. The previous artifact will no longer exist and is assigned a new ID [2]. 2. Matrix Scenarios: Because v6 allows overwriting, matrix jobs that use identical artifact names will cause subsequent jobs to overwrite the artifacts uploaded by previous jobs [1]. To avoid this and ensure all artifacts are preserved, it remains highly recommended to include unique identifiers in the artifact name—such as matrix suffixes (e.g., name: my-artifact-${{ matrix.os }}) [1][3]. 3. Immutable Artifacts: While individual uploads can be overwritten in v6, once an artifact has been successfully created, it is considered immutable within that specific upload action instance [1][2]. Note: If you explicitly want to prevent overwriting, some configurations or specific versions of actions may require you to ensure your naming conventions remain unique, as the overwrite behavior is now the default "last-write-wins" approach [1][2]. Top results: [1][5][2]

Citations:


Suffix the wheel artifact names with python-version.
actions/upload-artifact@v6 overwrites same-named artifacts, so the Linux, musllinux, and musllinux-cross matrix jobs keep only the last wheel set per target. Add -py${{ matrix.python-version }} to each artifact name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-binaries.yml at line 202, The wheel artifact names
in the build matrix are colliding across Python versions, so each upload should
be uniquely named per target and python-version. Update the artifact name used
by the upload step in the workflow to include the matrix python-version suffix,
using the existing matrix fields such as matrix.python-version so Linux,
musllinux, and musllinux-cross jobs preserve distinct wheel sets instead of
overwriting each other.

204-206: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show checkout-related sections and downstream containerized steps.
git grep -nE 'actions/checkout@|persist-credentials|docker-run-action|run-on-arch-action' -- .github/workflows/build-binaries.yml

echo
echo '--- build-binaries.yml excerpt ---'
nl -ba .github/workflows/build-binaries.yml | sed -n '180,380p'

Repository: tower/tower-cli

Length of output: 1156


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/build-binaries.yml')
for start, end in [(190, 235), (245, 285), (300, 340), (345, 375)]:
    print(f"\n--- lines {start}-{end} ---")
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4d}: {line}")
PY

Repository: tower/tower-cli

Length of output: 7520


🌐 Web query:

uraimo/run-on-arch-action workspace mount repository checkout credentials persist-credentials

💡 Result:

The uraimo/run-on-arch-action GitHub Action is designed to run commands within a container on non-x86 architectures [1][2]. It handles workspace mounting and repository access through standard GitHub Actions patterns rather than specific parameters within the action itself [3]. 1. Workspace Mount: The action automatically mounts the ${GITHUB_WORKSPACE} directory into the container [3]. If you need additional directories (like an artifacts directory), you must create them on the host using the setup property and then mount them into the container using the dockerRunArgs property [4][5][6]. Example: setup: | mkdir -p "${PWD}/artifacts" dockerRunArgs: | --volume "${PWD}/artifacts:/artifacts" 2. Repository Checkout: The repository is typically checked out using the standard actions/checkout action before calling run-on-arch-action [5]. 3. Persist Credentials: The persist-credentials setting is a parameter of the actions/checkout action, not run-on-arch-action [7]. When using actions/checkout, you can control whether the token is persisted in the local git configuration by setting persist-credentials: false (which is often recommended for security) [7]. Because run-on-arch-action mounts the ${GITHUB_WORKSPACE} (where the.git directory resides), any authentication settings or git configurations applied during the preceding checkout step remain available inside the container [3]. Important Note: When using run-on-arch-action, if you use the githubToken input to cache Docker images in your project's GitHub Package Registry, be aware that the cached image layer will be publicly available if your package registry is public [1][8]. Do not store secrets in the container state or logs, especially when using the install property [4][9].

Citations:


Disable persisted checkout credentials in the containerized build jobs.

musllinux and musllinux-cross both pass the checked-out workspace into addnab/docker-run-action / uraimo/run-on-arch-action, so the default checkout token remains available inside the container. linux-cross already sets persist-credentials: false; the same should be added here.

Suggested change
      - uses: actions/checkout@v6
        with:
          submodules: recursive
+         persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      - uses: actions/checkout@v6
        with:
          submodules: recursive
          persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 204-206: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-binaries.yml around lines 204 - 206, The checkout
step for the containerized build jobs still leaves persisted credentials
available in the workspace, unlike the existing linux-cross setup. Update the
actions/checkout usage in the musllinux and musllinux-cross job paths to disable
credential persistence by adding the same persist-credentials setting used
elsewhere, so the checked-out token is not exposed to addnab/docker-run-action
or uraimo/run-on-arch-action.

Source: Linters/SAST tools

crates/tower-cmd/src/api.rs (1)

381-405: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact credential payloads before calling the generic response helper.

vend_catalog_credentials now sends a bearer-token response through unwrap_api_response, whose debug path logs the raw response body. With --debug, this command will print credentials.oauth_token to stdout even when --show-token is omitted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tower-cmd/src/api.rs` around lines 381 - 405,
`vend_catalog_credentials` is passing a sensitive bearer-token response into
`unwrap_api_response`, which can expose `credentials.oauth_token` in debug logs.
Update `vend_catalog_credentials` (and, if needed, the `unwrap_api_response`
call path) to redact or avoid logging the raw response body for this API before
returning the `VendCatalogCredentialsResponse`, so debug output never prints the
token unless explicitly allowed by the command.
crates/tower-cmd/src/catalogs.rs (1)

241-247: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline crates/tower-cmd/src/catalogs.rs --view expanded

Repository: tower/tower-cli

Length of output: 1813


🏁 Script executed:

set -euo pipefail
ast-grep outline crates/tower-cmd/src/catalogs.rs --view expanded

Repository: tower/tower-cli

Length of output: 1813


🏁 Script executed:

set -euo pipefail
sed -n '225,270p' crates/tower-cmd/src/catalogs.rs
printf '\n--- tests ---\n'
sed -n '560,650p' crates/tower-cmd/src/catalogs.rs
printf '\n--- windows mentions ---\n'
rg -n "windows|Windows|win32|cfg\\(windows\\)" -S .

Repository: tower/tower-cli

Length of output: 50371


🏁 Script executed:

set -euo pipefail
sed -n '144,360p' crates/tower-cmd/src/catalogs.rs
printf '\n--- workspace/binary packaging ---\n'
rg -n "wix|windows|cfg\\(windows\\)|target.*windows|tower-cmd" Cargo.toml crates -S

Repository: tower/tower-cli

Length of output: 10490


Don't emit a POSIX-only token export snippet unconditionally.

token_export_command relies on export, shell quoting, and python3, so the hidden-token flow won't work for the Windows binaries shipped here. Render a platform-specific command or a manual set/copy instruction on Windows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tower-cmd/src/catalogs.rs` around lines 241 - 247, The token export
helper currently hardcodes a POSIX shell snippet in token_export_command, which
won’t work for Windows binaries. Update token_export_command to detect the
target platform and emit a platform-specific instruction: keep the existing
export-based flow for Unix-like shells, but on Windows return a
PowerShell-friendly or manual set/copy command instead of relying on export,
shell quoting, and python3. Use the existing token_export_command symbol as the
place to branch the output.
src/tower/_context.py (1)

65-83: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize blank auth env vars before session fallback.

With TOWER_API_KEY="" or TOWER_JWT="", Line 75 treats auth as present and skips _read_session(), but downstream auth checks still treat the empty value as missing. This breaks the tower login fallback for shells/CI that export empty vars.

Proposed fix
     def build(cls):
+        def getenv_or_none(name: str) -> Optional[str]:
+            return os.getenv(name) or None
+
         session = {}
-        tower_url = os.getenv("TOWER_URL")
+        tower_url = getenv_or_none("TOWER_URL")
         tower_environment = (
             os.getenv("TOWER__RUNTIME__ENVIRONMENT_NAME")
             or os.getenv("TOWER_ENVIRONMENT")
             or "default"
         )
-        tower_api_key = os.getenv("TOWER_API_KEY")
-        tower_jwt = os.getenv("TOWER_JWT")
-        tower_run_id = os.getenv("TOWER__RUNTIME__RUN_ID")
+        tower_api_key = getenv_or_none("TOWER_API_KEY")
+        tower_jwt = getenv_or_none("TOWER_JWT")
+        tower_run_id = getenv_or_none("TOWER__RUNTIME__RUN_ID")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    def build(cls):
        def getenv_or_none(name: str) -> Optional[str]:
            return os.getenv(name) or None

        session = {}
        tower_url = getenv_or_none("TOWER_URL")
        tower_environment = (
            os.getenv("TOWER__RUNTIME__ENVIRONMENT_NAME")
            or os.getenv("TOWER_ENVIRONMENT")
            or "default"
        )
        tower_api_key = getenv_or_none("TOWER_API_KEY")
        tower_jwt = getenv_or_none("TOWER_JWT")
        tower_run_id = getenv_or_none("TOWER__RUNTIME__RUN_ID")

        if tower_url is None or (tower_api_key is None and tower_jwt is None):
            session = _read_session()

        if tower_url is None:
            tower_url = (
                _get_session_value(session, "tower_url") or "https://api.tower.dev"
            )

        if tower_api_key is None and tower_jwt is None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tower/_context.py` around lines 65 - 83, Normalize empty auth environment
values in the `_context.py` initialization path by treating blank
`TOWER_API_KEY` and `TOWER_JWT` the same as unset before the `_read_session()`
fallback. Update the `tower_api_key`/`tower_jwt` handling in the context-loading
logic so `""` does not count as present, allowing `tower login` session values
to be used when env vars are exported empty. Keep the existing `tower_url` and
`tower_run_id` flow unchanged.
src/tower/_tables.py (1)

87-97: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Re-enter credential refresh for existing read-write catalogs.

After the first write, _catalog_mode becomes "read-write", so later writes/drop/create calls return the existing catalog without calling get_tower_catalog() again. That bypasses _storage’s expires_at refresh logic and can leave long-running table handles using expired short-lived credentials. Consider a refresh path that re-enters get_tower_catalog() for vended write operations while letting the credential cache decide whether a new vend is needed.

Also applies to: 486-498

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tower/_tables.py` around lines 87 - 97, The current read-write fast path
in _ensure_read_write_table prevents re-entering get_tower_catalog() once
_catalog_mode is already "read-write", which skips the _storage expires_at
refresh for long-lived handles. Update the write/drop/create flow so existing
read-write catalogs still go back through get_tower_catalog() when needed, while
letting the credential cache decide whether to vend new credentials. Keep the
fix centered on _ensure_read_write_table and the related read-write call sites
so refreshed storage credentials are picked up before subsequent operations.

@konstantinoscs konstantinoscs changed the title Add CLI commands and UX for Tower Storage functionality feat(cli+sdk): Tower Storage from the CLI — credential vending & connection snippets Jun 29, 2026
@konstantinoscs

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-trigger the failed workflow

@konstantinoscs konstantinoscs requested review from bradhe, giray123, jo-sm, sammuti and socksy and removed request for jo-sm June 29, 2026 14:13
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

@konstantinoscs Got it — I’ll re-run the PR review and retry posting the inline comments on #309.

(╯°□°)╯︵ 🐇

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tower-cmd/src/catalogs.rs`:
- Around line 241-247: The token export helper in token_export_command always
shells out to bare tower --json catalogs credentials, which discards any active
custom Tower endpoint used earlier. Update the generated export command to
preserve and reuse the current Tower URL context when fetching credentials, so
the TOWER_CATALOG_TOKEN comes from the same backend as the catalog metadata;
locate the fix in token_export_command and its format string.
- Around line 321-335: The hidden-token DBeaver snippet is using the wrong
variable syntax and will not resolve as emitted. Update the dbeaver_token branch
in crates/tower-cmd/src/catalogs.rs to use DBeaver’s ${...} placeholder format,
matching the existing hidden-token style used by sql_token and dbt_token, while
keeping the show_token path unchanged.

In `@src/tower/_context.py`:
- Around line 18-22: The session loading fallback in the context builder does
not treat undecodable session files as invalid data, so a corrupted session file
can still break context construction. Update the session read path in the
context-loading logic around the json.load(session_file) call to also catch
UnicodeDecodeError together with the existing file and JSON exceptions, and
return an empty session state in that case.

In `@src/tower/_storage.py`:
- Around line 67-73: Expired credential entries in the caching path are never
removed, so update the cache handling in the credential lookup flow around
_credential_cache, _CachedCredentials, and is_usable to evict any unusable entry
before storing the newly fetched credentials. In the branch that checks cached
credentials, remove the stale cache_key when cached.is_usable(now) is false,
then proceed to call _vend_with_default_catalog_fallback and store the fresh
_CachedCredentials as before.

In `@src/tower/_tables.py`:
- Around line 87-97: The write-escalation path in _ensure_read_write_table and
_ensure_catalog_mode can skip credential refresh once _catalog_mode is already
"read-write", which leaves long-lived Table/TableReference instances using stale
storage credentials. Update the read-write transition logic so write operations
still route through get_tower_catalog() or otherwise consult _storage for
refresh-before-expiry behavior, and avoid returning early solely because the
mode already matches. Use _ensure_read_write_table, _ensure_catalog_mode, and
get_tower_catalog() as the key places to adjust the refresh behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ffb1ff46-0477-4416-875a-8387a8db061c

📥 Commits

Reviewing files that changed from the base of the PR and between eb47c53 and 0594a45.

📒 Files selected for processing (14)
  • crates/tower-cmd/src/api.rs
  • crates/tower-cmd/src/catalogs.rs
  • crates/tower-cmd/src/lib.rs
  • crates/tower-cmd/src/mcp.rs
  • crates/tower-cmd/src/templates/dbeaver.txt.tmpl
  • crates/tower-cmd/src/templates/dbt.yml.tmpl
  • crates/tower-cmd/src/templates/duckdb.sql.tmpl
  • crates/tower-cmd/src/templates/pyiceberg.py.tmpl
  • crates/tower-cmd/src/templates/spark.py.tmpl
  • src/tower/_context.py
  • src/tower/_storage.py
  • src/tower/_tables.py
  • tests/tower/test_storage.py
  • tests/tower/test_tables.py

Comment thread crates/tower-cmd/src/catalogs.rs Outdated
Comment thread crates/tower-cmd/src/catalogs.rs
Comment thread src/tower/_context.py
Comment thread src/tower/_storage.py
Comment thread src/tower/_tables.py
@konstantinoscs konstantinoscs force-pushed the feature/tow-2315-add-cli-commands-and-ux-for-storage-functionality branch from 0594a45 to 19c080a Compare June 29, 2026 15:14
@tower tower deleted a comment from coderabbitai Bot Jun 29, 2026
@tower tower deleted a comment from coderabbitai Bot Jun 29, 2026
@konstantinoscs

Copy link
Copy Markdown
Contributor Author

@coderabbitai you're not functionaing well today. I've adressed all of your feedback, check and resolve the comments

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

The first Comment image shows a terminal session on tower-cli running:

cargo run -- catalogs list --storage

The output shows the Rust dev profile finishing successfully, then the CLI printing:

  • Listing catalogs... Done!
  • A table with columns Name, Type, and Environment
  • One row: default1 tower-catalog default

The second Comment image shows another terminal session running:

cargo run -- catalogs show default1 --environment default

The output again shows a successful Rust run, then details for the catalog:

  • Catalog: default1
  • Type: tower-catalog
  • Environment: default

Under Properties, it shows a table with columns Name, Runtime Var, and Preview, including:

  • uriPYICEBERG_CATALOG__DEFAULT1__URI
  • warehousePYICEBERG_CATALOG__DEFAULT1__WAREHOUSE
  • credentialPYICEBERG_CATALOG__DEFAULT1__CREDENTIAL
  • scopePYICEBERG_CATALOG__DEFAULT1__SCOPE
  • ro_credentialPYICEBERG_CATALOG__DEFAULT1__RO_CREDENTIAL
  • ro_scopePYICEBERG_CATALOG__DEFAULT1__RO_SCOPE

The preview values are partially redacted with XXXXXX....

@konstantinoscs Sorry about the earlier confusion. Thanks for the follow-up and for sharing the verification screenshots — I’m resolving my review comments now.

(;ω;)🐇

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

.long("type")
.value_parser(value_parser!(String))
.help("Filter catalogs by type, e.g. tower-catalog")
.action(ArgAction::Set),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we have both clap::ArgAction::Set on line 23, and ArgAction::Set on this line - we should be consistent imo

)
}

fn print_credentials(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a function better suited for output.rs?

Comment thread crates/tower-cmd/src/catalogs.rs Outdated
return;
}

print_credentials(name, &env, mode, &response.credentials, format, show_token);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't seem to be any error logic if the credentials are failed to be fetched?

)
.await;

if output::get_output_mode().is_json() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a smell that the print_credentials should be in output.rs and then takes care of whether to print out json inside of it to keep this irrelevant logic out of this file

Comment thread crates/tower-cmd/src/catalogs.rs Outdated
Comment thread src/tower/_context.py
try:
with session_path.open() as session_file:
session = json.load(session_file)
except (FileNotFoundError, OSError, json.JSONDecodeError, UnicodeDecodeError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, why special case these errors if we're not differentiating between them?

Comment thread src/tower/_context.py
)

if tower_api_key is None and tower_jwt is None:
tower_jwt = _session_jwt(session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something seems super complicated here in all this logic getting session etc - IIRC in the cli we read from the env var, then the .config/tower/ session.json in turn, and state is just held by the fact that the latter can change (the former cannot, but TOWER_API_KEY isn't typically used in the scenario where that matters, which is a long running process — atm that's only the MCP server that e.g. needs to know that the team token changes when it changes team.

Here I feel it's already quite complicated (why do we have a context object with a build method? Doesn't feel pythonic at all to me) and on top of that I don't know why we need to support the sessions — is it expected that e.g. changing team in the CLI should somehow affect the local SDK? Would there be a scenario in which we want someone to setup this SDK and expect them to use the CLI session?

Comment thread src/tower/_storage.py
return


def _unwrap_vend_result(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just rely on the existing 404 error? needing to do this adds this function, and the need for _is_not_found() one-liner. Assuming there's an AI agent involved, can i maybe interest you in my simpler agent skill? https://github.com/socksy/agent-skills

Comment thread src/tower/_tables.py
table,
table_reference=self if self._tower_vended else None,
table_identifier=table_name,
catalog_mode=self._catalog_mode,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this always be "read-write" since you just ensured it above?

Comment thread src/tower/_tables.py

namespace = namespace_or_default(self._namespace)
table_name = make_table_name(self._name, namespace)
catalog = self._ensure_catalog_mode("read-write")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it rather make sense to do this in the constructor function where we assign the catalog into the object? Seems weird to me to need to make the caller site do a sanitization when i would just expect to do things on the instance variables of the model? It doesn't seem there's any error in this path that you'd want to be lazily done

URI: __TOWER_URI__
Warehouse: __TOWER_WAREHOUSE__
Authentication: Bearer token
Token: __TOWER_TOKEN__

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you need a scope field here

@@ -0,0 +1,9 @@
duckdb <<SQL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duckdb can also do vended credential flow on the terminal
Something like

duckdb <<SQL
INSTALL httpfs;
LOAD httpfs;
INSTALL iceberg;
LOAD iceberg;

SET s3_region='eu-central-1';

CREATE OR REPLACE SECRET tower_cat (
  TYPE iceberg,
  CLIENT_ID '__TOWER_CLIENT_ID__',
  CLIENT_SECRET '__TOWER_CLIENT_SECRET__',
  OAUTH2_SCOPE 'PRINCIPAL_ROLE:ALL',
  ENDPOINT '__TOWER_URI__'
);

ATTACH '__TOWER_WAREHOUSE__' AS __TOWER_NAME__ (
  TYPE iceberg,
  SECRET tower_cat,
  DEFAULT_REGION 'eu-central-1',
  ACCESS_DELEGATION_MODE 'vended_credentials'
);
SQL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants