Skip to content

feat(config): api_key_cmd / auth_token_cmd — resolve LLM credential from a command (#236)#13

Open
chethanuk wants to merge 3 commits into
mainfrom
feat/api-key-cmd
Open

feat(config): api_key_cmd / auth_token_cmd — resolve LLM credential from a command (#236)#13
chethanuk wants to merge 3 commits into
mainfrom
feat/api-key-cmd

Conversation

@chethanuk

@chethanuk chethanuk commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Fork-side staging + review PR for upstream alibaba#236. Upstream PR intentionally held until the maintainer signs off on the §C5 design proposal.

Lets users fetch the LLM provider key via a command (op read, pass show) instead of storing it plaintext in config.json — same pattern as git credential.helper / AWS credential_process.

Design

  • Precedence (single site tryProviderConfig, presets + custom alike): static api_key always wins (stderr warning if a command is also set) → api_key_cmd → preset env var → error. Legacy llm block gets a mirrored auth_token_cmd; an incomplete legacy block never runs the command, a set-but-failing command on a complete block is a hard error.
  • Execution: build-tag split (sh -c / cmd /C), 60s timeout, child stderr passes through (pinentry/1Password/op prompts stay visible), stdout trimmed and used in memory only — never logged or written. Empty / whitespace-only / multi-line / timed-out output are all hard errors. No caching.
  • config set supports + round-trips the fields (not masked — they're command lines, not secrets); TUI clone preserves api_key_cmd; docs in configuration.md (en/zh/ja).

Verification

  • go build ./... + GOOS=windows go build ./internal/llm/... (Windows arm compile-checked; CI is Linux-only)
  • go test ./internal/llm/... ./cmd/opencodereview/... — pass (table-driven runner matrix: success/trim/non-zero/empty/whitespace/multi-line/not-found/timeout; resolver precedence + legacy-fallthrough rows)
  • go vet clean; make coverage 81.3% (≥80% gate)

Summary by CodeRabbit

  • New Features

    • Added api_key_cmd for dynamically fetching provider API keys and auth_token_cmd for legacy LLM auth tokens via shell commands.
    • Static api_key / auth_token take precedence (with warnings when both static and command forms are set).
    • Command stdout is required to be a successful, single-line non-empty value (enforced with a 60-second timeout); otherwise resolution fails.
  • Documentation

    • Updated configuration docs (EN/JA/ZH) with precedence rules and command execution requirements.
  • Tests

    • Expanded unit coverage for provider/legacy resolution, precedence, failure modes, timeouts, and incomplete legacy configs.

Add `api_key_cmd` (provider entries) and `auth_token_cmd` (legacy llm
block) so the LLM credential can be fetched from a secret manager at
review time instead of stored plaintext in config.json — same pattern as
git credential.helper / AWS credential_process.

Resolution precedence (single site, presets and custom providers alike):
static api_key always wins (stderr warning if a command is also set) →
api_key_cmd → preset env var → error. The legacy llm block gets a
mirrored auth_token_cmd; an incomplete legacy block never executes the
command, and a set-but-failing command on a complete block is a hard
error (never a silent fallback).

Command execution is a build-tag split (sh -c / cmd /C) with a 60s
timeout; the child's stderr passes through so pinentry/1Password/op
prompts stay visible. Stdout is trimmed and used in memory only — never
written to config or logged. Empty, whitespace-only, multi-line, and
timed-out output are all hard errors. No caching (resolution runs once
per process).

- config set: api_key_cmd/auth_token_cmd are settable and round-trip;
  not masked (they are command lines, not secrets).
- TUI cloneProviderEntry preserves api_key_cmd.
- docs: 'API key from a command' section in configuration.md (en/zh/ja).

Tests: table-driven runner matrix (success/trim/non-zero/empty/
whitespace/multi-line/not-found/timeout) + resolver precedence and
legacy-fallthrough rows. Coverage 81.3%; Windows arm compile-checked
(CI is Linux-only).
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds command-based retrieval for provider API keys and legacy LLM auth tokens, including configuration fields, platform-specific execution, timeout and output validation, resolution precedence, tests, cloning support, and multilingual documentation.

Changes

Command-based credential resolution

Layer / File(s) Summary
Credential command configuration
cmd/opencodereview/config_cmd.go, cmd/opencodereview/provider_tui.go, cmd/opencodereview/provider_tui_funcs_test.go
Adds api_key_cmd and auth_token_cmd configuration fields, setter support, provider-field validation, and clone preservation tests.
Credential command execution
internal/llm/keycmd.go, internal/llm/keycmd_*.go, internal/llm/keycmd_test.go
Executes shell commands with platform-specific shells, a 60-second timeout, stderr forwarding, strict single-line output validation, and unit tests for success and failure cases.
Endpoint credential resolution
internal/llm/resolver.go, internal/llm/resolver_keycmd_test.go
Resolves provider and legacy credentials using static-value precedence, command execution, environment fallback rules, and incomplete-configuration handling, with integration tests.
Configuration documentation
pages/src/content/docs/*/configuration.md
Documents command-based credential retrieval, precedence, output constraints, timeout behavior, failures, and stderr forwarding in English, Japanese, and Chinese.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ResolveEndpoint
  participant tryProviderConfig
  participant resolveKeyCmd
  participant ShellCommand
  ResolveEndpoint->>tryProviderConfig: resolve provider endpoint
  tryProviderConfig->>resolveKeyCmd: resolve api_key_cmd
  resolveKeyCmd->>ShellCommand: execute credential command
  ShellCommand-->>resolveKeyCmd: return validated stdout credential
  resolveKeyCmd-->>tryProviderConfig: provide API key
  tryProviderConfig-->>ResolveEndpoint: return endpoint with token
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: resolving LLM credentials from commands via api_key_cmd/auth_token_cmd.
Description check ✅ Passed The description covers purpose, design, execution, docs, and verification, though it doesn't follow the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-key-cmd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add the two coverage gaps from review: a CRLF-terminated command output
row in the runner matrix (asserts \r\n fully trimmed), and stderr-warning
assertions for the both-set case on both the provider path (api_key +
api_key_cmd) and the legacy path (auth_token + auth_token_cmd), via an
inlined os.Pipe stderr-capture helper.
@chethanuk

Copy link
Copy Markdown
Owner Author

Adversarial review pass complete (precedence, command-execution safety, error hygiene, edge cases, config round-trip, TUI persistence, docs) — verdict: ship-ready, no correctness bugs.

Confirmed correct: static api_key always wins with a non-fatal both-set warning; the resolved credential can never reach any log/error string (c.Stderr = os.Stderr keeps ExitError.Stderr empty and stdout is discarded on error); masking correctly hides api_key/auth_token but not the *_cmd command lines (and the new field didn't regress the suffix match); the legacy path short-circuits before running the command on an incomplete block and uses the command-resolved token in the final endpoint; no TUI serialize path drops api_key_cmd; docs match the code across en/zh/ja.

Closed two review-flagged test gaps in ace73aa: a CRLF-output row in the runner matrix, and stderr both-set-warning assertions on both the provider and legacy paths (via an inlined os.Pipe capture helper).

Left as documented judgment (not fixed): unbounded stdout buffer and grandchild-kill on timeout — both are the user's own local config (same trust as any shell command they'd run), and the common credential tools (op, pass) neither stream nor background; io.LimitReader/process-group kill would be defense-in-depth against a self-inflicted fat-finger, not worth the complexity in a reference feature.

Gates: go build + GOOS=windows go build ✓, go test ./internal/llm/... ./cmd/opencodereview/... ✓, go vet ✓, make coverage 81.3% (≥80%).

Holding the upstream PR until the maintainer signs off on the §C5 design proposal; ready to open it against that the moment they approve.

Satisfy Go doc-comment convention (and CodeRabbit's docstring gate) for the
per-OS credential-command runner; comments only, no behavior change.

@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: 2

🤖 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 `@internal/llm/keycmd_unix.go`:
- Around line 10-11: Update newKeyCmd in the Unix implementation to create the
shell in its own process group using SysProcAttr.Setpgid, then ensure context
cancellation terminates that entire process group rather than only the shell.
Preserve the existing sh -c invocation and credential-command timeout behavior.

In `@internal/llm/resolver_keycmd_test.go`:
- Around line 66-69: Update captureStderr to close the read end r after os.Pipe
succeeds, using deferred cleanup so every invocation releases the file
descriptor while preserving the existing pipe and stderr-capture 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4512d2c-7a62-4360-87bf-fdfd8c6a3275

📥 Commits

Reviewing files that changed from the base of the PR and between d30f4f9 and fdf21fc.

📒 Files selected for processing (4)
  • internal/llm/keycmd_test.go
  • internal/llm/keycmd_unix.go
  • internal/llm/keycmd_windows.go
  • internal/llm/resolver_keycmd_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/llm/keycmd_windows.go
  • internal/llm/keycmd_test.go

Comment on lines +10 to +11
// newKeyCmd builds the OS-specific shell invocation (sh -c on Unix) that runs a
// credential command under ctx, so its timeout and cancellation are honored.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the process execution configuration for Setpgid usage
ast-grep outline internal/llm/keycmd_unix.go
cat internal/llm/keycmd_unix.go

Repository: chethanuk/open-code-review

Length of output: 595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search for Setpgid / process-group handling =="
rg -n "Setpgid|SysProcAttr|Setctty|Credential.*cmd|newKeyCmd|CommandContext\\(" internal . || true

echo
echo "== behavior probe: does killing /bin/sh terminate its child? =="
python3 - <<'PY'
import os, signal, subprocess, time, sys

# Spawn a shell that launches a long-lived child and then waits.
# The shell itself should exit when terminated, but the child should keep running
# if it does not receive a forwarded signal.
p = subprocess.Popen(
    ["sh", "-c", "sleep 60"],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
)

time.sleep(0.2)
pid = p.pid

# Find child PID via /proc, if available.
child = None
proc_status = f"/proc/{pid}/task/{pid}/children"
try:
    with open(proc_status) as f:
        data = f.read().strip()
        if data:
            child = int(data.split()[0])
except Exception as e:
    print(f"could not read child pid from {proc_status}: {e}")

os.kill(pid, signal.SIGKILL)
try:
    p.wait(timeout=2)
except subprocess.TimeoutExpired:
    print("shell did not exit after SIGKILL")
    sys.exit(1)

print(f"shell exited with code {p.returncode}")
if child is not None:
    alive = os.path.exists(f"/proc/{child}")
    print(f"child pid={child} alive_after_shell_kill={alive}")
PY

Repository: chethanuk/open-code-review

Length of output: 4150


Kill the credential command’s process group on Unix exec.CommandContext(ctx, "sh", "-c", cmd) only stops the shell; the credential command can keep running after cancellation. Set SysProcAttr.Setpgid here and terminate the whole group on cancel.

🤖 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 `@internal/llm/keycmd_unix.go` around lines 10 - 11, Update newKeyCmd in the
Unix implementation to create the shell in its own process group using
SysProcAttr.Setpgid, then ensure context cancellation terminates that entire
process group rather than only the shell. Preserve the existing sh -c invocation
and credential-command timeout behavior.

Comment on lines +66 to +69
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close the read end of the pipe to prevent file descriptor leaks.

The read end of the pipe (r) is never closed, which leaks a file descriptor each time captureStderr is called. While tests are short-lived, it is good practice to clean up resources to prevent file descriptor exhaustion in larger test suites.

🔧 Proposed fix
 	r, w, err := os.Pipe()
 	if err != nil {
 		t.Fatalf("os.Pipe: %v", err)
 	}
+	defer r.Close()
📝 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.

Suggested change
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}
defer r.Close()
🤖 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 `@internal/llm/resolver_keycmd_test.go` around lines 66 - 69, Update
captureStderr to close the read end r after os.Pipe succeeds, using deferred
cleanup so every invocation releases the file descriptor while preserving the
existing pipe and stderr-capture behavior.

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.

1 participant