diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index 61b13a7..23cf1e1 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -719,18 +719,19 @@ if err != nil { } target := inspection.EffectiveModelParams +if inspection.APIKeyRequired { + // Arrange a usable direct or configured environment credential. +} if target.APIKeyEnv != "" { - // This is a configured optional environment lookup source. -} else if inspection.APIKeyRequired { - // Arrange a direct credential before later execution. + // This is the configured environment lookup source, not its value. } ``` Use this configuration-time boundary when only the profile and its target need checking. Use `Prepare` when the application also needs prompt, input, schema, or rendering work; use prepared execution when that work must remain tied to a -later execution. A reported `APIKeyEnv` is a configured optional source, while -`APIKeyRequired` is the explicit local requirement. The +later execution. A reported `APIKeyEnv` identifies a configured source, while +`APIKeyRequired` is the explicit requirement; both may be present. The [credential format reference](../formats.md#credentials) and the method's [GoDoc](../../engine.go) own the exact precedence, timing, result, and error contracts. diff --git a/docs/integrations/openai-compatible-chat.md b/docs/integrations/openai-compatible-chat.md index 482ce95..79c01c0 100644 --- a/docs/integrations/openai-compatible-chat.md +++ b/docs/integrations/openai-compatible-chat.md @@ -115,9 +115,10 @@ The client conditionally includes: strict flag, and schema document. Plain JSON validation does not add an object-only response constraint. -The engine resolves backend, profile, and request extra-parameter maps by -whole-map replacement rather than key merging. The resulting effective map is -merged into the top-level body. Empty keys, values that cannot be encoded, and +Configured profile maps recursively overlay backend defaults, while a nonempty +runtime override replaces the complete configured map. The [format reference](../formats.md#defaults-and-overrides) +owns these resolution rules. The resulting effective map is merged into the +top-level body. Empty keys, values that cannot be encoded, and collisions with these reserved fields are rejected before any provider call: - `model` diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d4594a0..b56cf9c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -715,6 +715,11 @@ Go 1.27.3; an injected-observer test verifies permit release before callbacks. boundary, and changed documentation links and semantics agree with canonical GoDoc/formats. Do not snapshot whole diagnostic strings or unrelated JSON fields. +Completion: marker-based request formatting and JSON checks cover value and +pointer forms. Profile credential inspection, exact IDs, prompt listing, and +provider-map precedence prose now match the implemented contracts. Focused +privacy, profile, inheritance, and provider tests pass on Go 1.27.3. + ## Stage 28: Give Bundled Catalog Expectations One Test Owner **Catalog:** 28. **Scope:** catalog tests, not bundled asset changes. diff --git a/engine.go b/engine.go index 4f57a74..9f8874a 100644 --- a/engine.go +++ b/engine.go @@ -312,8 +312,8 @@ func (e *Engine) InspectPrompt( // override. BackendID is empty for an endpoint-only profile. // // APIKeyEnv in the returned target is an environment-variable name, never its -// value. APIKeyRequired instead reports a direct credential requirement and is -// mutually exclusive with a nonblank APIKeyEnv. InspectProfile neither derives +// value. APIKeyRequired reports that a credential is required; an explicit +// profile environment source can also be present. InspectProfile neither derives // an ID from a prompt default_profile nor checks credential availability, so an // absent or blank named environment variable is not an error. // diff --git a/listing.go b/listing.go index 96733f3..ad925a6 100644 --- a/listing.go +++ b/listing.go @@ -93,8 +93,8 @@ func (e *Engine) ListProfiles(ctx context.Context, filters ...NamespaceFilter) ( // behavior as [Engine.ListProfiles], using the prompt namespace by default. // At most one filter is accepted. // -// The returned slice and each Version pointer are caller-owned and safe to -// mutate while the engine is used concurrently. Empty inventories succeed. +// The returned slice is caller-owned and safe to mutate while the engine is +// used concurrently. Empty inventories succeed. // // Listing reports membership, not execution readiness. It does not reload // configuration or perform resource, credential, provider, or execution work. diff --git a/request_privacy_contract_test.go b/request_privacy_contract_test.go new file mode 100644 index 0000000..6184e85 --- /dev/null +++ b/request_privacy_contract_test.go @@ -0,0 +1,46 @@ +package promptkit_test + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "go.maximumdirect.net/promptkit" +) + +func TestRunRequestFormattingAndJSONProtectDirectCredential(t *testing.T) { + markers := []string{"secret-key-marker", "variable-marker", "artifact-body-marker", "message-body-marker", "setting-marker", "schema-path-marker"} + request := promptkit.RunRequest{ + PromptID: "prompt", ProfileID: "profile", APIKey: markers[0], + Vars: map[string]string{"name": markers[1]}, + Inputs: map[string]promptkit.ArtifactRef{"input": {Type: promptkit.ArtifactRefInline, Body: markers[2]}}, + AppendedMessages: []promptkit.RenderedMessage{{Role: promptkit.RoleUser, Content: markers[3]}}, + Execution: &promptkit.ExecutionTargetOverride{Model: markers[4]}, + Validation: &promptkit.OutputContract{SchemaPath: markers[5]}, + } + for _, rendered := range []string{ + fmt.Sprintf("%v", request), fmt.Sprintf("%+v", request), fmt.Sprintf("%#v", request), + fmt.Sprintf("%v", &request), fmt.Sprintf("%+v", &request), fmt.Sprintf("%#v", &request), + request.String(), request.GoString(), (&request).String(), (&request).GoString(), + } { + for _, marker := range markers { + if strings.Contains(rendered, marker) { + t.Fatalf("request formatting exposes %q: %s", marker, rendered) + } + } + } + for _, value := range []any{request, &request} { + encoded, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var object map[string]any + if err := json.Unmarshal(encoded, &object); err != nil { + t.Fatal(err) + } + if _, found := object["APIKey"]; found || strings.Contains(string(encoded), markers[0]) { + t.Fatal("request JSON exposed the direct credential") + } + } +} diff --git a/types.go b/types.go index 533df8c..0ad4fa2 100644 --- a/types.go +++ b/types.go @@ -363,7 +363,7 @@ type ExecutionTarget struct { // separate from that target to preserve ExecutionTarget's general execution // and stable JSON contracts. type ProfileInspection struct { - // ProfileID is the trimmed, exact profile ID inspected by the engine. + // ProfileID is the exact profile ID inspected by the engine. ProfileID string // EffectiveModelParams contains settings resolved from the framework timeout // baseline, selected backend, and then profile, without a request override. @@ -371,9 +371,9 @@ type ProfileInspection struct { // provider default. APIKeyEnv is an environment-variable name, never its // credential value. EffectiveModelParams ExecutionTarget - // APIKeyRequired reports that a later execution must supply a direct API - // key or an explicit request environment override. It is mutually exclusive - // with a nonblank EffectiveModelParams.APIKeyEnv. + // APIKeyRequired reports that a later execution must supply a usable direct + // key or environment credential. An explicit profile environment source can + // coexist with this requirement. APIKeyRequired bool }