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:
- a nonblank direct
RunRequest.APIKeyremains the highest-precedence credential and producesAuthorization: Bearer <key>; - otherwise, a nonblank effective
APIKeyEnvis read when generation begins; - a nonblank trimmed environment value produces the bearer header;
- an absent, empty, or whitespace-only optional environment value produces no
Authorizationheader and does not prevent preparation or execution; and - the provider response follows the normal success or error path, including
public
GenerationErrorconversion 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
APIKeyEnvis 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.
APIKeyRequiredremains the only existing explicit local requirement flag. Do not add a backend field, YAML field, request field, or new public API.- With
APIKeyRequiredtrue:- a nonblank direct key satisfies the requirement;
- an explicitly selected nonblank
APIKeyEnvsatisfies 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
ErrAPIKeyEnvMissingandErrInvalidRequestat the public boundary.
ErrAPIKeyEnvMissingremains 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
Authorizationheader at all when no usable credential exists. It must not sendBearerwith an empty value. - Injected
LLMClientbehavior 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
- Update
validateAPIKeyininternal/usecase/runner.gowith this policy:- return success immediately for a nonblank direct key;
- return success when
apiKeyRequiredis false, regardless of whetherapiKeyEnvis blank, populated, or missing; - when
apiKeyRequiredis true and the trimmed environment name is blank, return the existingErrAPIKeyRequired; - when
apiKeyRequiredis true and the selected environment variable is unset, empty, or whitespace-only, return the existing wrappedErrAPIKeyEnvMissingdiagnostic naming the variable; and - otherwise return success.
- Keep the calls from preparation and
RunPreparedin place. Do not move environment lookup into backend/profile resolution, admission, the domain model, or the root facade. - Do not clear
APIKeyEnvfrom effective or prepared metadata merely because its current value is absent. The name remains the configured lookup source; the secret value remains excluded. - Preserve credential-source precedence and
APIKeyRequiredmerge 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:
- Replace
TestRunnerRunAPIKeyEnvMissingEnvironmentValueFailsClearlywith a test proving an unset optional profileAPIKeyEnvreaches 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. - Preserve the existing populated-environment and direct-key-precedence tests.
- Preserve
TestRunnerPrepareAPIKeyRequiredFailsWithoutDirectKeyand the existing direct-key success coverage. - Revise the prepared credential test matrix to prove both distinct cases:
- an optional environment value that disappears after
PrepareExecutiondoes not prevent admission and generation; and - an
APIKeyRequiredprofile using an explicit requestAPIKeyEnvoverride still fails withErrInvalidRequestandErrAPIKeyEnvMissingif that value disappears beforeRunPrepared, before admission or generation.
- an optional environment value that disappears after
- 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
- Refactor the authentication branch in
internal/llm/openai_compatible_client.goso 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.
- trimmed
- Set
AuthorizationtoBearer <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 tohttp.Client.Do. - Retain defensive enforcement for direct internal callers of the transport:
- if
req.Target.APIKeyRequiredis true and no usable source exists, returnErrInvalidRequestbefore 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.
- if
- 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. - Allow an unauthenticated upstream non-2xx response to flow through the
existing
ProviderHTTPErrorand publicGenerationErrormachinery 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
ErrInvalidRequestbefore 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
- Update the GoDoc for
ErrAPIKeyEnvMissinginengine.goso it applies only when an explicitly required credential names an unset or empty environment variable. Retain the exported value and itsErrInvalidRequestidentity. - Update credential GoDoc where the exact public semantics are exposed:
Backend.APIKeyEnvinbackends.go;ExecutionTarget.APIKeyEnvandExecutionTargetOverride.APIKeyEnvintypes.go; and- any nearby
APIKeyRequiredwording that would otherwise imply every named environment source is mandatory. State that optional missing values cause the built-in client to omitAuthorization; do not promise that injected clients resolve environment variables identically.
- Replace
TestMissingCredentialsFailClearlyWhenProfileRequiresAuthinengine_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 whoseAPIKeyEnvis explicitly empty in the process environment, and have the controlled upstream return a structured authentication failure. Assert:- the request reaches upstream;
Authorizationis absent;- the result is nil;
- the error does not match
ErrInvalidRequestorErrAPIKeyEnvMissing; and - the error matches
ErrLLMGenerateand is discoverable as a*GenerationErrorwith the upstream status and representative provider detail.
- 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:
- In
docs/formats.md, define profile, backend, and requestapi_key_envvalues as optional lookup sources. In the credentials section, distinguish them fromAPIKeyRequired, 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. - 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. - 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. - In
docs/consumers/pkg-promptkit.md, clarify near profile inspection or credential guidance that a reportedAPIKeyEnvis a configured optional source, whileAPIKeyRequiredis the explicit local requirement. Link to the exact public GoDoc or format reference rather than reproducing the full precedence contract. - 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.