Files
deepseek-harness/packages/credentials/credentials
Yichen Jiang 90c3118302 fix(credentials-local): one operation chain, read-modify-write under the shared writer lock, and a quote-aware line editor
Review round three, credentials half. dsh-atomic-write grows the
cross-process writer-lock primitive (withFileLock: wx sentinel, bounded
backoff, stale takeover via onStaleBreak, deadline failure) plus a dirMode
option, and settings-local migrates its private copy to it; both providers
now create harness-home directories 0700.

credentials-local reuses the reviewed settings-local shape: watcher
reloads and line edits share one settled operation chain; every write
re-reads the document under the lock and publishes unobserved external
entries before editing, so an edit inside the debounce window (or another
process's write) can never be overwritten; the watcher's ready signal
queues one reconcile closing the startup gap.

The line editor is now physical-line aware: continuation lines of a
quoted multi-line value are never mistaken for assignments, untouched
lines keep their exact bytes (CRLF included), an edited line keeps its
own terminator, and appends use the document's dominant ending. A
multi-line entry reports writable: false, matching what set() would do.

The Credentials base class owns a contained notifyUpdated fan-out:
providers publish only after the commit, every listener runs, sync throws
and async rejections are logged without failing the committed write, and
INVARIANT-coded failures rethrow after the fan-out.
2026-07-30 15:40:09 +08:00
..

dsh-credentials

English | 中文

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

import type { Context } from 'cordis'
import { credentialRef } from '@deepseek-ai/dsh-credentials'

declare const ctx: Context

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 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, through the consuming LLM adapters: a resolved value authorizes their provider requests, and the 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.