Command-backed provider credentials — the api_key_command config key.
A static api_key (or a ${NAME} reference to one) is only good for a
long-lived secret. Short-lived SSO/gateway tokens come from a credential
HELPER instead: a small program that prints a fresh token on stdout. This
namespace runs that helper for a provider and hands the trimmed stdout back
as the API key.
Three contracts hold this together:
ProcessBuilder verbatim. It is never joined, never split on
whitespace, and never handed to sh -c, so a token containing shell
metacharacters — or a config written by someone else — cannot become
command injection.:api-key), never logged, and never placed in an error
message. Every diagnostic this namespace produces is built from argv[0],
the exit code, and the helper's stderr — never its stdout.timeout-ms, with a successful token cached for
success-ttl-ms and a failure remembered for failure-ttl-ms. A
long-running gateway must not fork a token helper per turn, and an
interactive helper must never be launched twice concurrently.resolve! never throws: callers use its :error to render a provider as
unavailable (providers/provider-status, doctor) or to drop it from the
router build, exactly as an unresolved ${NAME} is handled today.
Command-backed provider credentials — the `api_key_command` config key.
A static `api_key` (or a `${NAME}` reference to one) is only good for a
long-lived secret. Short-lived SSO/gateway tokens come from a credential
HELPER instead: a small program that prints a fresh token on stdout. This
namespace runs that helper for a provider and hands the trimmed stdout back
as the API key.
Three contracts hold this together:
- **No shell, ever.** The configured value is a structured argv that is
passed to `ProcessBuilder` verbatim. It is never joined, never split on
whitespace, and never handed to `sh -c`, so a token containing shell
metacharacters — or a config written by someone else — cannot become
command injection.
- **The credential is write-once, in memory.** Resolved stdout is returned
to the caller and cached HERE. It is never persisted (nothing writes it
back into `:api-key`), never logged, and never placed in an error
message. Every diagnostic this namespace produces is built from argv[0],
the exit code, and the helper's stderr — never its stdout.
- **Bounded and single-flight.** One helper invocation per provider at a
time, bounded by `timeout-ms`, with a successful token cached for
`success-ttl-ms` and a failure remembered for `failure-ttl-ms`. A
long-running gateway must not fork a token helper per turn, and an
interactive helper must never be launched twice concurrently.
`resolve!` never throws: callers use its `:error` to render a provider as
unavailable (`providers/provider-status`, `doctor`) or to drop it from the
router build, exactly as an unresolved `${NAME}` is handled today.(argv v)Normalize an api_key_command config value into a non-empty argv vector, or
nil when it is absent or malformed.
A bare string is ONE argv element — it is deliberately NOT shell-split. The
whole point of the structured form is that arguments are unambiguous, and
silently splitting foo --env 'a b' would reintroduce exactly the quoting
guesswork this key exists to avoid. Every element must be a non-blank string;
anything else makes the value invalid rather than partially usable.
Normalize an `api_key_command` config value into a non-empty argv vector, or nil when it is absent or malformed. A bare string is ONE argv element — it is deliberately NOT shell-split. The whole point of the structured form is that arguments are unambiguous, and silently splitting `foo --env 'a b'` would reintroduce exactly the quoting guesswork this key exists to avoid. Every element must be a non-blank string; anything else makes the value invalid rather than partially usable.
(invalidate! pid)Forget pid's cached credential so the next resolve! re-runs the helper.
This is the refresh trigger: a 401 means the short-lived token the router baked in has expired, and the only way to learn the new one is to ask the helper again. Dropping the entry (rather than re-running here) keeps refresh lazy, single-flight, and on the thread that actually needs the token.
Forget `pid`'s cached credential so the next `resolve!` re-runs the helper. This is the refresh trigger: a 401 means the short-lived token the router baked in has expired, and the only way to learn the new one is to ask the helper again. Dropping the entry (rather than re-running here) keeps refresh lazy, single-flight, and on the thread that actually needs the token.
(peek-token pid v)Cached result for pid WITHOUT running anything: {:token _}, {:error _},
or nil when nothing valid is cached.
This is the read for synchronous UI paths (initial-provider-status), which
must never fork a subprocess to paint a frame — an unknown verdict there
correctly renders as loading.
Cached result for `pid` WITHOUT running anything: `{:token _}`, `{:error _}`,
or nil when nothing valid is cached.
This is the read for synchronous UI paths (`initial-provider-status`), which
must never fork a subprocess to paint a frame — an unknown verdict there
correctly renders as `loading`.(reset-cache!)Drop every cached credential. For /reload and for tests.
Drop every cached credential. For `/reload` and for tests.
(resolve! pid v)Resolve provider pid's credential from config value v, cached and
single-flight. Returns {:token s} or {:error non-secret-message}; returns
nil when v configures no command at all. Never throws.
BLOCKS for up to timeout-ms on a cold miss, so callers on a paint path want
peek-token instead.
Resolve provider `pid`'s credential from config value `v`, cached and
single-flight. Returns `{:token s}` or `{:error non-secret-message}`; returns
nil when `v` configures no command at all. Never throws.
BLOCKS for up to `timeout-ms` on a cold miss, so callers on a paint path want
`peek-token` instead.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |