Skip to content

Accept per-field service_account in BigQuery credentials - #94

Merged
ashwin-agami merged 2 commits into
mainfrom
fix/bigquery-service-account-alias
Jul 9, 2026
Merged

ashwin-agami merged 2 commits into
mainfrom
fix/bigquery-service-account-alias

Conversation

@ashwin-agami

Copy link
Copy Markdown
Contributor

What

A per-field service_account = /path/key.json line in a BigQuery credentials profile was silently ignored — the executor reads service_account_path, so the client fell back to Application Default Credentials instead of using the key. The docs use the shorter service_account spelling, making this an easy footgun.

_load_credentials now normalizes the service_account / credentials_path aliases to service_account_path for the per-field INI form — matching what _parse_dsn already does for the url = bigquery://...?service_account=... form. An explicit service_account_path still wins if both are present.

Why

The three spellings were already equivalent in the DSN path but not the per-field path — inconsistent, and the inconsistency failed closed (fell back to ADC) with no error, so users couldn't tell why their key wasn't used.

Changes

  • packages/agami-core/src/execute_sql.py — alias normalization in _load_credentials (source of truth).
  • plugins/agami/lib/execute_sql.py — re-synced vendored copy (dev.py sync-lib); drift check passes.
  • tests/test_env_credentials.py — two tests: alias normalizes from the file; explicit service_account_path isn't clobbered.

Test plan

  • uv run dev.py check — full suite green (1326 passed), ruff + gitleaks + lib-drift all pass.
  • New tests cover both the alias-maps and explicit-wins cases.

Customer-safety

No real names/data/credentials — neutral my-proj / /abs/path/key.json placeholders only.

The BigQuery executor reads `service_account_path`, and the DSN parser
already maps `service_account` / `credentials_path` to it — but the
per-field INI loader passed the section through verbatim, so a
`service_account = /path/key.json` line was silently ignored and the
client fell back to ADC. The docs use the shorter `service_account`
spelling, making this an easy footgun.

Normalize the aliases to `service_account_path` in `_load_credentials`
(explicit `service_account_path` still wins). Vendored lib re-synced.

Tests: alias normalizes from the file; explicit path isn't clobbered.
Copilot AI review requested due to automatic review settings July 8, 2026 06:24

Copilot AI 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.

Pull request overview

This PR fixes an inconsistency in BigQuery credential loading: the per-field INI profile form now treats service_account and credentials_path as aliases for service_account_path, matching DSN parsing behavior and preventing silent fallback to Application Default Credentials.

Changes:

  • Normalize per-field service_account / credentials_path to service_account_path in _load_credentials.
  • Re-sync the vendored plugins/agami/lib/execute_sql.py copy to match core behavior.
  • Add tests covering per-field alias normalization and “explicit path wins” semantics.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/agami-core/src/execute_sql.py Adds per-field alias normalization so BigQuery credentials consistently use service_account_path.
plugins/agami/lib/execute_sql.py Mirrors the same alias normalization in the vendored executor copy.
tests/test_env_credentials.py Adds coverage for per-field BigQuery alias behavior and precedence rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +127 to +145


def test_bigquery_explicit_service_account_path_is_not_clobbered(monkeypatch, tmp_path):
"""If both `service_account_path` and the alias appear, the explicit path wins."""
creds = tmp_path / "credentials"
creds.write_text(
"[gcp]\ntype = bigquery\nproject = my-proj\n"
"service_account_path = /explicit/key.json\n"
"service_account = /alias/key.json\n",
encoding="utf-8",
)
import os
if os.name == "posix":
creds.chmod(0o600)
monkeypatch.setattr(execute_sql, "CREDENTIALS_PATH", creds)

assert _load_credentials("gcp")["service_account_path"] == "/explicit/key.json"


Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in d0a9ddb (landed after this review): the test is now @pytest.mark.parametrize("alias", ["service_account", "credentials_path"]), so both aliases are covered in the per-field/INI path. Mutation-checked — dropping the credentials_path arm of the loop fails the parametrized case.

Address the SDLC review's test-quality findings:
- Cover the `credentials_path` alias too (was untested) via parametrize.
- Rename/clarify the precedence test so it pins the `not …get(...)` guard;
  mutation-checked that dropping the guard now fails it.
- Add the empty-value edge (`service_account =`) → stays unset, ADC path.

Test-only change; production logic unchanged.
@ashwin-agami

Copy link
Copy Markdown
Contributor Author

Agami SDLC review — Review stage (panel)

Ran the standard review skills (code-review / silent-failure / test-analyzer / security-review) + the Agami rubric. This assists; human code-owner approval still required.

Standard skills

  • Correctness / silent-failure — clean. Traced the url = + per-field interaction and alias precedence; explicit service_account_path wins regardless of INI key order; empty/whitespace values are .strip()-ed and fall through to ADC. No swallowed errors.
  • Security — clean. chmod-600 gate and the key-file permission check are untouched and run after normalization; no new path is logged/echoed; no PII/real creds in tests (placeholders only); zero-egress preserved.
  • Test quality — 2 must-fix, both now fixed in d0a9ddb:
    • credentials_path alias was untested → now parametrized over both aliases (mutation-checked: dropping the arm fails it).
    • precedence test was weak → renamed + strengthened to pin the not …get(...) guard (mutation-checked: dropping the guard fails it). Added the empty-value edge.

Agami rubric

  • Ports-and-adapters ✓ (shared core loader; no app.api/app.mcp_server imports) · Both surfaces in sync ✓ (vendored plugins/agami/lib re-synced, drift check green) · Right-sized ✓ (one slice, ~50 lines) · Conventions ✓ (typed, snake_case, full-sentence comments, regression test lands with the fix, no weakened tests) · High-lane data rules ✓ (credential handling reviewed, no leak/egress).
  • nit — no Spec: <PROJECT>-NNN link; acceptable for a small standalone bug-fix, flagging for the record.

Agami review complete — 0 outstanding must-fix (2 raised, 2 fixed), 1 nit. Human PR approval still required.

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@ashwin-agami
ashwin-agami merged commit 9915471 into main Jul 9, 2026
7 checks passed
@ashwin-agami
ashwin-agami deleted the fix/bigquery-service-account-alias branch July 9, 2026 08:41
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants