Files
promptkit/docs/roadmap/implementation.md

16 KiB

Optional API-Key Environment Implementation Plan

Purpose

Change Promptkit's credential handling so an effective APIKeyEnv names an optional credential source rather than implicitly requiring that environment variable to be populated. When neither a direct key nor a populated optional environment variable is available, the built-in OpenAI-compatible client must omit the Authorization header and let the upstream provider accept or reject the unauthenticated request.

This document owns implementation sequencing for that change. Follow the architecture, documentation, and testing policies under docs/policy/ and the task-specific reading guide in docs/development.md throughout the work.

Target Outcome

For ordinary and prepared execution:

  1. a nonblank direct RunRequest.APIKey remains the highest-precedence credential and produces Authorization: Bearer <key>;
  2. otherwise, a nonblank effective APIKeyEnv is read when generation begins;
  3. a nonblank trimmed environment value produces the bearer header;
  4. an absent, empty, or whitespace-only optional environment value produces no Authorization header and does not prevent preparation or execution; and
  5. the provider response follows the normal success or error path, including public GenerationError conversion for a non-2xx response.

An explicitly required credential retains local validation. APIKeyRequired continues to require either a nonblank direct key or a populated environment variable explicitly selected by the request. Missing required credentials fail before provider transport.

Fixed Decisions

  • APIKeyEnv is a lookup location, not an assertion that authentication is required. This applies whether the name comes from a built-in backend, a consumer backend, a file profile, or a request override.
  • An environment value is usable only after strings.TrimSpace; unset, empty, and whitespace-only values are equivalent and cause header omission when optional.
  • Direct keys retain precedence. When a direct key is nonblank, Promptkit does not need the environment value and must not let a missing environment value block the request.
  • APIKeyRequired remains the only existing explicit local requirement flag. Do not add a backend field, YAML field, request field, or new public API.
  • With APIKeyRequired true:
    • a nonblank direct key satisfies the requirement;
    • an explicitly selected nonblank APIKeyEnv satisfies it only when that variable currently contains a nonblank value;
    • no selected environment name retains the existing required-key failure; and
    • a selected but unset environment retains ErrAPIKeyEnvMissing and ErrInvalidRequest at the public boundary.
  • ErrAPIKeyEnvMissing remains exported for compatibility but is narrowed to a missing environment credential in an explicitly required flow. Optional missing environment values do not return it.
  • Optional credential availability is not frozen during preparation. The built-in client reads the selected environment variable for each generation, including repair generation. Prepared execution therefore uses the value visible when it runs.
  • Required prepared execution continues to recheck credential availability before admission. Optional prepared execution proceeds even if the variable becomes unset after preparation.
  • The built-in transport sets no Authorization header at all when no usable credential exists. It must not send Bearer with an empty value.
  • Injected LLMClient behavior is not redefined. Promptkit stops rejecting an optional missing environment before the injected client is called and continues to pass the effective target through the public adapter without resolving or exposing a secret on the client's behalf.
  • Upstream authentication policy remains upstream. Promptkit adds no provider- specific authentication rules, retry behavior, status mapping, or special handling beyond the existing structured non-2xx error path.

Execution Rules

  • Complete the stages in numerical order. Each stage is scoped for one coding- agent prompt and must finish its focused tests before the next stage begins.
  • Treat all stages as one behavior change. Intermediate stages intentionally leave the use-case and transport policies temporarily different; do not tag, release, or claim the change is complete until Stage 3 passes.
  • At the start of each stage, inspect the working tree and preserve all unrelated changes. In particular, merge rather than overwrite any existing edits in files touched by this plan.
  • Use controlled transports, local servers, and t.Setenv; never contact a live provider or depend on the developer machine's credentials.
  • Update existing tests whose asserted policy is changing instead of retaining contradictory tests or adding duplicate coverage under new names.
  • Do not add dependencies, commit, tag, push, or edit release documentation unless separately instructed.
  • Current-state prose documentation changes only in Stage 3, after the runtime behavior exists. GoDoc changes alongside the public contract in that stage.

Stage 1: Make Use-Case Credential Validation Requirement-Aware

Objective

Stop preparation and execution orchestration from treating every named environment variable as required, while preserving the explicit APIKeyRequired and prepared-execution contracts.

Implementation

  1. Update validateAPIKey in internal/usecase/runner.go with this policy:
    • return success immediately for a nonblank direct key;
    • return success when apiKeyRequired is false, regardless of whether apiKeyEnv is blank, populated, or missing;
    • when apiKeyRequired is true and the trimmed environment name is blank, return the existing ErrAPIKeyRequired;
    • when apiKeyRequired is true and the selected environment variable is unset, empty, or whitespace-only, return the existing wrapped ErrAPIKeyEnvMissing diagnostic naming the variable; and
    • otherwise return success.
  2. Keep the calls from preparation and RunPrepared in place. Do not move environment lookup into backend/profile resolution, admission, the domain model, or the root facade.
  3. Do not clear APIKeyEnv from effective or prepared metadata merely because its current value is absent. The name remains the configured lookup source; the secret value remains excluded.
  4. Preserve credential-source precedence and APIKeyRequired merge behavior. A request environment override remains capable of satisfying an in-memory profile's explicit requirement when its value is populated.

Tests

Update the narrow internal/usecase behavior owners rather than adding tests for the private helper itself:

  1. Replace TestRunnerRunAPIKeyEnvMissingEnvironmentValueFailsClearly with a test proving an unset optional profile APIKeyEnv reaches the injected model client and completes successfully. Explicitly set the named variable to an empty value so the test is independent of the process environment.
  2. Preserve the existing populated-environment and direct-key-precedence tests.
  3. Preserve TestRunnerPrepareAPIKeyRequiredFailsWithoutDirectKey and the existing direct-key success coverage.
  4. Revise the prepared credential test matrix to prove both distinct cases:
    • an optional environment value that disappears after PrepareExecution does not prevent admission and generation; and
    • an APIKeyRequired profile using an explicit request APIKeyEnv override still fails with ErrInvalidRequest and ErrAPIKeyEnvMissing if that value disappears before RunPrepared, before admission or generation.
  5. Keep prepared handles single-use in both outcomes and avoid duplicating unrelated preparation, validation, or admission assertions.

Verification

go test ./internal/usecase -run 'APIKey|Credential|Prepared'
go test ./internal/usecase
go test ./...

Stage 1 is complete when optional missing environment values no longer block the use-case layer and explicit requirements retain their prior identities and timing.

Status: Complete.

Stage 2: Omit Authentication in the Built-In Client When Optional Keys Are Missing

Objective

Make the OpenAI-compatible transport implement the optional lookup contract at the boundary that owns the outbound Authorization header.

Implementation

  1. Refactor the authentication branch in internal/llm/openai_compatible_client.go so it resolves one credential in this order:
    • trimmed req.Target.APIKey; then
    • the trimmed value of the environment variable named by the trimmed req.Target.APIKeyEnv.
  2. Set Authorization to Bearer <credential> only when the resolved value is nonblank. If the direct key is blank and the optional environment lookup is absent, empty, or whitespace-only, leave the header unset and continue to http.Client.Do.
  3. Retain defensive enforcement for direct internal callers of the transport:
    • if req.Target.APIKeyRequired is true and no usable source exists, return ErrInvalidRequest before transport;
    • use the existing missing-environment diagnostic when a nonblank environment name was selected; and
    • use a concise required-key diagnostic when no environment name exists. Do not add an internal or public error type for this branch.
  4. Preserve request construction, endpoint validation, timeout behavior, response-body ownership, and direct-key redaction. Never serialize APIKeyEnv, APIKeyRequired, or a credential into the JSON request body.
  5. Allow an unauthenticated upstream non-2xx response to flow through the existing ProviderHTTPError and public GenerationError machinery without adding a credential-specific translation.

Tests

Update TestOpenAICompatibleClientAuthentication in internal/llm/openai_compatible_client_test.go as the transport contract owner. Its table should cover only the meaningful credential states:

  • a direct key takes precedence over a populated environment value;
  • a populated environment value supplies the bearer header when no direct key is present;
  • no configured source sends the request without Authorization;
  • a named but unset optional environment sends the request without Authorization;
  • a named but unset required environment fails with ErrInvalidRequest before transport; and
  • a required target with no source also fails before transport.

Use exact header assertions because presence or absence of Authorization is the wire contract. Do not repeat endpoint, body, timeout, or response-error matrices in this test.

Verification

go test ./internal/llm -run TestOpenAICompatibleClientAuthentication
go test ./internal/llm
go test ./...

Stage 2 is complete when the built-in transport sends unauthenticated requests for optional missing sources, still enforces explicit requirements, and all existing provider-response behavior remains green.

Status: Complete.

Stage 3: Align the Public Contract, Durable Documentation, and Full Validation

Objective

Prove the assembled consumer workflow, update canonical contract owners, and complete repository-wide validation.

Public Contract And Tests

  1. Update the GoDoc for ErrAPIKeyEnvMissing in engine.go so it applies only when an explicitly required credential names an unset or empty environment variable. Retain the exported value and its ErrInvalidRequest identity.
  2. Update credential GoDoc where the exact public semantics are exposed:
    • Backend.APIKeyEnv in backends.go;
    • ExecutionTarget.APIKeyEnv and ExecutionTargetOverride.APIKeyEnv in types.go; and
    • any nearby APIKeyRequired wording that would otherwise imply every named environment source is mandatory. State that optional missing values cause the built-in client to omit Authorization; do not promise that injected clients resolve environment variables identically.
  3. Replace TestMissingCredentialsFailClearlyWhenProfileRequiresAuth in engine_test.go, whose old assertion is intentionally obsolete, with one focused external-package contract test. Assemble a real engine with a controlled HTTP client or local server, select a backend or file profile whose APIKeyEnv is explicitly empty in the process environment, and have the controlled upstream return a structured authentication failure. Assert:
    • the request reaches upstream;
    • Authorization is absent;
    • the result is nil;
    • the error does not match ErrInvalidRequest or ErrAPIKeyEnvMissing; and
    • the error matches ErrLLMGenerate and is discoverable as a *GenerationError with the upstream status and representative provider detail.
  4. Keep this root test representative. The transport authentication table owns the full credential matrix, and the existing generation-error tests own envelope parsing, bounds, and formatting.

Documentation

Update each canonical owner only for its topic:

  1. In docs/formats.md, define profile, backend, and request api_key_env values as optional lookup sources. In the credentials section, distinguish them from APIKeyRequired, document header omission for a missing optional value, retain direct-key precedence, and state that required availability is validated during preparation and rechecked for prepared execution.
  2. In docs/integrations/openai-compatible-chat.md, document the outbound authentication wire behavior: a bearer header is sent only for a usable direct or environment credential; otherwise the header is omitted and the provider response is handled normally.
  3. In docs/internal/llm.md, update the internal flow and failure categories to distinguish optional omission from explicit required-key rejection. Preserve any unrelated edits already present in this file.
  4. In docs/consumers/pkg-promptkit.md, clarify near profile inspection or credential guidance that a reported APIKeyEnv is a configured optional source, while APIKeyRequired is the explicit local requirement. Link to the exact public GoDoc or format reference rather than reproducing the full precedence contract.
  5. Do not change architecture policy, backend IDs/default names, release notes, the README, or examples unless implementation uncovers a concrete inaccurate current-state statement in one of those owners.

Final Validation

Run the complete maintainer workflow from docs/development.md#maintainer-validation:

go test ./...
go test -race ./...
go vet ./...
go build ./...
go run ./examples/go-library/prepare
go run ./examples/go-library/run

Also run the documented tracked-Go formatting check, local Markdown-link validation, repository-hygiene checks, ignored-file check, credential scan, and git diff --check. Review the complete diff and confirm that:

  • optional absent, empty, and whitespace-only environment values omit authentication and reach transport;
  • direct and populated environment credentials still produce the correct bearer header;
  • explicit required flows still fail locally with the intended identities;
  • ordinary, prepared, built-in, injected-client, and repair paths follow their stated ownership boundaries;
  • upstream non-2xx responses remain structured generation errors rather than local credential errors;
  • no credential value is serialized, retained in public metadata, or exposed by formatting; and
  • durable documentation describes only the now-implemented behavior with one canonical owner per exact contract.

Stage 3 is complete when the public workflow, documentation, and full maintainer validation all match the target outcome.

Open Questions

None. Optional environment lookup, explicit requirement behavior, precedence, prepared-execution timing, outbound header semantics, error compatibility, documentation ownership, and test boundaries are fixed by this plan.