feat(credentials): abstract credential seam (ctx.credentials)
References-not-values doctrine: settings carry env-shaped CredentialRefs, providers own storage. Per-operation resolve, UI-safe describe, fail-loud set/unset under read-only shadowing, credentials/updated commit event with a live-service invariant.
This commit is contained in:
45
packages/credentials/credentials/README.md
Normal file
45
packages/credentials/credentials/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# dsh-credentials
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Abstract credential seam (`ctx.credentials`). One doctrine, three consequences:
|
||||
|
||||
**Configuration carries references to secrets, never the secrets.** A settings section or `cordis.yml` entry says `apiKeyEnv: DEEPSEEK_API_KEY`; the value behind that reference lives with a credential provider. So the settings document stays safe to sync and to render in a configuration UI, `describe()` can answer "is this configured, where from, can I write it" without ever holding a value, and rotating a secret touches no configuration file.
|
||||
|
||||
**Consumers resolve per operation.** `resolve(ref)` is called at the start of each operation (the LLM adapters resolve once per model request) and never cached across operations — that read is what makes a changed credential reach the very next request without restarting any plugin.
|
||||
|
||||
**An empty stored value is absent.** Everywhere: `resolve` skips it, `describe` reports it unconfigured. A blank can never masquerade as a configured secret.
|
||||
|
||||
## Surface
|
||||
|
||||
```ts
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
|
||||
const ref = credentialRef('DEEPSEEK_API_KEY') // POSIX shell identifier, branded
|
||||
const hit = await ctx.credentials.resolve(ref) // { value, source } | undefined
|
||||
const info = await ctx.credentials.describe(ref) // { configured, source?, writable } — never the value
|
||||
await ctx.credentials.set(ref, 'sk-…') // rejects while a read-only source shadows the ref
|
||||
await ctx.credentials.unset(ref) // no-op when absent; same shadowing rule
|
||||
```
|
||||
|
||||
`credentials/updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration UIs refreshing a "configured" badge.
|
||||
|
||||
The shadowing rule on `set`/`unset` is deliberate fail-loud: when a read-only source (the live process environment, in the local provider) currently supplies the reference, a write would appear to succeed while resolution keeps returning the shadowing value — the seam rejects instead, and `describe().writable` lets a UI render the reference read-only up front.
|
||||
|
||||
## Providers
|
||||
|
||||
[`dsh-credentials-local`](../credentials-local/README.md) layers the live process environment over a `$DSH_HOME/.env` file. The seam shape leaves room for keyring-, helper-command-, and KMS-backed providers; a remote settings provider never needs to carry secrets.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly: a resolved value authorizes provider requests; the consuming adapter owns every model-visible surface.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; credentials never enter a request prefix.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No enumeration** — the seam answers questions about references it is given; configuration surfaces learn the references from settings schemas, so a `list()` has no current consumer.
|
||||
- **References are environment-variable-shaped** — one flat POSIX-identifier namespace until a provider needs richer addressing.
|
||||
- **Process-environment changes are invisible** — no event can fire for them; a UI only re-reads `describe()` on its own navigation.
|
||||
Reference in New Issue
Block a user