Skip to content

feat: secure-by-default endpoint trust for ambient credentials - #590

Merged
Seth Juarez (sethjuarez) merged 15 commits into
mainfrom
sethjuarez/secure-by-default-credentials
Oct 21, 2026
Merged

Seth Juarez (sethjuarez) merged 15 commits into
mainfrom
sethjuarez/secure-by-default-credentials

Conversation

@sethjuarez

Copy link
Copy Markdown
Member

Summary

Prompty now refuses to load a .prompty file that would send ambient credentials to an endpoint chosen by the file. Ambient credentials are ones the file doesn't supply itself, such as DefaultAzureCredential tokens, *_API_KEY environment variables, or OAuth/Foundry identity.

The rule lives in the spec (§2.5.1), is pinned by conformance vectors in schema/model/conformance/vectors/load.tsp, and is implemented the same way in all 7 runtimes (Python, TypeScript, C#, Go, Java, Rust, Swift).

Rule (spec §2.5.1)

The check runs on the raw model.connection, before any ${...} is resolved.

  • Skip the check when the connection is absent or kind: reference.
  • File-supplied credential: kind: key with a non-blank literal apiKey. Anything else counts as ambient, including ${env:...} keys, blank keys, oauth, foundry, anonymous and custom kinds.
  • File-controlled destination: an endpoint or tokenUrl given as a literal, a ${file:} reference, or an ${env:VAR:default} whose default is used because VAR is unset.
  • An ambient credential sent to a file-controlled destination that isn't trusted fails with the load error untrusted_endpoint.

Trusted by default (https only): api.openai.com, api.anthropic.com, *.openai.azure.com, *.services.ai.azure.com, *.cognitiveservices.azure.com, login.microsoftonline.com.

Matching compares scheme + host + port exactly. A URL containing a backslash, or one that can't be parsed, is rejected.

Opt-out / migration

This is a behavior change. A file that pairs ambient credentials with a custom endpoint now fails to load until that endpoint is trusted. To trust it, use either:

  • the trustedEndpoints load option (named per each runtime's conventions), or
  • the PROMPTY_TRUSTED_ENDPOINTS environment variable (comma-separated origins; * trusts all).

Files that load the endpoint from the environment (endpoint: ${env:AZURE_OPENAI_ENDPOINT}), or that supply a literal key, keep working unchanged.

Validation

  • All unit and conformance suites pass in all 7 runtimes.
  • Format and lint are clean: ruff, prettier, eslint/tsc, rustfmt, clippy, dotnet format, gofmt.
  • npm run generate regenerates with no diff.

Release notes

This isn't marked as breaking (!), to avoid a major-version bump across runtimes. The changelog should call out the new load-time check and the opt-out.

Add spec section 2.5.1 Endpoint Trust, the untrusted_endpoint load error, the trustedEndpoints load option, and load conformance vectors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add core/endpoint_trust.py and a trusted_endpoints option on load()/load_async(); the vector adapter passes trustedEndpoints.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Some executors trim the connection apiKey and fall back to environment
credentials when it is empty. A whitespace-only literal key therefore
must not count as file-supplied. Update spec 2.5.1, add a conformance
vector, and align the gate in all runtimes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment on lines +454 to +457
catch (Exception error)
{
caught = error;
}
Comment on lines +468 to +471
catch (Exception error)
{
caught = error;
}
Comment on lines +482 to +485
catch (Exception error)
{
caught = error;
}
Comment on lines +496 to +499
catch (Exception error)
{
caught = error;
}
Comment on lines +510 to +513
catch (Exception error)
{
caught = error;
}
Comment on lines +524 to +527
catch (Exception error)
{
caught = error;
}
Comment on lines +538 to +541
catch (Exception error)
{
caught = error;
}
Comment on lines +552 to +555
catch (Exception error)
{
caught = error;
}
Comment on lines +566 to +569
catch (Exception error)
{
caught = error;
}
Comment on lines +580 to +583
catch (Exception error)
{
caught = error;
}
@sethjuarez
Seth Juarez (sethjuarez) marked this pull request as ready for review October 21, 2026 02:53
@sethjuarez
Seth Juarez (sethjuarez) merged commit b26eed4 into main Oct 21, 2026
39 checks passed
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