Credentials, Settings, Storage, and Telemetry
Unsexy and full of traps: credentials resolved every time, secrets never land in config.
Play first, then talk. Top: the credentials file on disk. Below, two processes issuing requests: left is DSH—every request re-reads the key from the store; right is the common pattern—read once at boot, cache forever. Mid-run the script rotates the key, then clears it. Hit Play and watch each side’s fate.
packages/llm/llm-deepseek/src/index.ts lines 241–245. Real DSH has no “read once at boot” process on the right—that’s the cautionary foil.First, one fact: nowhere in DSH settings or cordis.yml sits an API key value. They carry references—POSIX-style env names like DEEPSEEK_API_KEY. Values belong to the credential provider; the local provider searches four layers: process env first, then $DSH_HOME/.credentials.yaml, then project and user .env. That’s the subtitle’s “secrets never land in config”: only names persist; secrets stay outside config (docs/subsystems/credentials.zh.md line 5).
Then the lesson’s most important rule: consumers re-resolve the reference on every operation—never cache across operations. The docs say it plainly: that per-operation read is the hot-reload mechanism (same doc, line 20). On the DeepSeek adapter that’s packages/llm/llm-deepseek/src/adapter.ts lines 214–222: at each stream() start, freeze connection config and key into one snapshot for that request’s whole life; the next request re-resolves automatically.
The edge cases the outline asks about get their answers here. Rotate the key mid-request and this request finishes on the old key; the new key starts on the next request—no half-old/half-new. And because the key is resolved inside the connection snapshot, endpoint and secret always come from the same generation—no hybrid of new endpoint with old key on config rollback (the comment there states that intent).
Two easy-to-miss seam-level rules. First, an empty stored value is treated as absent everywhere—setting the key to "" equals unset, and the next request raises MISSING_CREDENTIAL (the demo’s last step). Second, the settings UI uses describe(ref): it returns only “set or not, which layer, writable?”—never the value; refs supplied by process env report writable: false, because writing there would look successful while resolve still returns the env’s old value, so the seam refuses up front (same doc, line 34).
What shows this architecture’s cleanliness is the credentials/updated event (same doc, line 50). Credential changes do emit it, but the docs say consumers don’t need it—it only refreshes the settings UI’s “configured” badge. Hot reload rides on read timing, not notification broadcasts—no invalidation messages to chase, no subscriptions to manage.
Resolve per operationRotate keys without restart; the next request picks up the new value. In-flight requests finish on the same-generation snapshot—endpoint and secret never hybridize.
Empty = unsetSeam-level rule, consistent everywhere. Missing key → MISSING_CREDENTIAL naming the config entry points; describe answers everything but never echoes the value.
One anonymous id, three consumersOTel’s user.id, /feedback receipts, and the DeepSeek request header share one UUID—lazy-created: if never successfully used, it never hits disk.
This sits inside resolveApiKey (from line 225); every model request walks it: if the credentials seam is mounted, resolve through it; if not, fall back to launch env vars. Note the else-branch comment—without the seam there’s no ranked managed store, so the environment is the whole credential plane:
if (credentials !== undefined) {
const hit = await credentials.resolve(ref)
if (hit !== undefined) return assertUsableApiKey(hit.value, 'llm-deepseek', ref)
} else {
// Without the seam there is no managed store to rank against, so the
// environment is the whole credential plane.
const ambient = launchEnvironmentOf(ctx).get(ref)
if (ambient !== undefined && ambient.value.length > 0) {
return assertUsableApiKey(ambient.value, 'llm-deepseek', ref)
}
}
If both paths miss, what throws next is MISSING_CREDENTIAL (lines 241–245)—the error names both config entry points; the last red line on the demo’s left is that original text.
deepseek-harness-master repo; verified against packages/llm/llm-deepseek/src/index.ts, verified on 2026-08-13. Code blocks keep the original source text; excerpt from the resolveApiKey function body.Settings are another trap. You edit settings.yaml in an editor while the web UI edits too—two harness processes may be open. A naive write serializes the in-memory snapshot straight back; last writer wins and wipes the earlier whole file: the config you just added in the editor vanishes on the other process’s save.
DSH’s write path blocks that (Agent Note 2026-07-30-settings-write-path-integrity.md). Before every disk write it re-reads disk, merges external edits, then finishes a full “read, render, atomic commit” under a cross-process file lock. The lock is withFileLock: exclusively create <filename>.lock with wx—success means you hold it; if held, exponential backoff from an initial delay up to a cap, then error. Readers don’t contend for the lock; commit is atomic rename of a temp file—readers always see a complete version.
One detail worth pausing on: after lock wait times out, it errors rather than deleting someone else’s lock file. The comment above the function explains—lock-file age doesn’t prove its owner is dead; stealing a live lock is far more dangerous than waiting out; cleaning orphan locks is an ops action. Familiar recipe: when unsure, fail loudly rather than silently cause havoc.
Source:packages/util/atomic-write/src/index.ts withFileLock, lines 86–111; verified on 2026-08-13.
The KV store’s SQLite backend keeps the same version stance. STORAGE_SQLITE_SCHEMA_VERSION is currently 1, in PRAGMA user_version; on open, a brand-new empty DB gets the current stamp, and any other version is refused—no in-place migration. Same philosophy as last lesson’s session-log versions: unreleased software has no history that must be preserved—better to refuse plainly than carry a pile of migrators.
One small hard tradeoff: journal mode defaults to WAL; bad filesystems can fall back to several rollback-journal modes, but memory and off are excluded at the type level (same file, lines 23–29 comments). One-line reason: throwing away journal durability silently violates the persistence clause in the KV backend contract. Fast is fine; fast enough to lie isn’t.
Telemetry’s worst failure mode is stealing the show. DSH makes it an optional capability seam: not on the agent-loop spine, no telemetry content enters model requests, and the harness’s duty ends at emit() (docs/subsystems/session-telemetry.zh.md). Each record passes a redaction pipeline before export; deployers hang rule listeners; listener exceptions are fail-closed—drop that record, don’t send. Redaction only touches the export copy; the authoritative session log isn’t changed by a single character.
Finally, anonymous identity. A random UUID v4 lives at $DSH_HOME/.anonymous-user-id, shared by three consumers: OTel’s user.id, /feedback confirmation receipts, and every DeepSeek x-deepseek-harness-user-id header (packages/identity/anonymous-user-id/README.zh.md). One shared id lets the receiving side correlate all three streams without minting three identities.
The clever bit is when it’s created. In llm-deepseek the id is lazy—userId ??= getOrCreateAnonymousUserId()—the file appears only on first real use (index.ts lines 248–249); and in stream() credential resolve runs before identity resolve (adapter.ts lines 221–222). Together: a machine that never had a key fails at credentials, and no tracking identity appears on disk for free. Minting an id before the tool has done a single thing for you—DSH doesn’t do that.
Grok Build
Credentials go through AuthCredentialProvider (crates/codegen/xai-grok-auth/src/auth_provider.rs). The interface docs ask implementers for a cheap disk re-read before every snapshot so sibling processes like grok-desktop and grok login can surface new credentials—same direction as DSH’s per-operation re-resolve.
It also has a post-hoc safety net: refresh_after_unauthorized()—on 401 try refreshing the token and retry once, mainly for expiring OAuth. Resolve-before plus retry-after is far steadier than cache-alone.
Claude Code
Its homework is at boot: utils/secureStorage/keychainPrefetch.ts fires a macOS Keychain read in parallel at process start, overlapping ~135ms of module import, and only waits when business code actually needs it—turning a ~200ms serial read into near-zero (study ch.1 startup analysis).
Optimization points the opposite way from DSH: it cares how fast that one boot read is; DSH cares how correct the next read is after rotation. Terminal products restart cheaply and rotate rarely—prefetch plus cache pays; infrastructure processes stay up long, and restart interrupts every session—per-operation resolve pays. Both are right for different scenes.
You rotated the key—why didn’t it take
Your deploy export DEEPSEEK_API_KEY=oldkey in the boot script, then later wrote a new value to .credentials.yaml via the web Models page. Now the old key leaked and must be revoked urgently—you enter a new key on Models, save succeeds, but the next request still uses the old one. Walk why: among four layers, process env wins; a newer file still ranks below it. And how the UI could have saved you: describe reports this ref as writable: false, so the UI can render the field read-only up front and you wouldn’t fill it for nothing. The real fix is change the launch env—or don’t put this variable in the environment.
emit(), redaction is fail-closed, and one lazy anonymous id serves three consumers—never written if unused.