describe() now carries each namespace's detached composition base and raw
user section beside the resolved value — presence in the user layer is how
a form marks a field user-overridden — and describe({redactSecrets:true})
strips role('secret') fields from every layer while enumerating their
{path,set} slots, so a wire surface has no slot that can carry a secret.
The pure redactSecrets(schema,value) walker (object/dict/array containers,
secret-role subtree as opaque leaf, inputs never mutated) is exported for
any other wire; the README's no-redaction Known Limitation is discharged.
@deepseek-ai/dsh-settings
English | 中文
Abstract user-settings seam (ctx.settings). One provider holds a raw document of per-namespace sections; plugins register a namespace schema and read a resolved value layered as schema defaults, then the registrant's composition base (its cordis.yml entry-config subset), then the user document section. Without a mounted provider nothing changes for consumers: they keep resolving entry config alone, so every composition works with or without settings.
Service API
register(ns, schema, { base?, applies? })— returns the ownerSettingsScope(get/watch/update). The registration is an effect on the calling plugin's fiber: disposing that fiber removes the namespace and its observers. A stored section the schema rejects fails the registration itself; a duplicate namespace fails loud.describe(options?)— one descriptor per namespace (schema.toJSON()envelope, resolved value, detachedbase/userlayers,applies) for configuration surfaces; a field's presence inuseris what marks it user-overridden.describe({ redactSecrets: true })stripsrole('secret')fields from every layer and adds thesecretsslot list ({ path, set }); every wire surface MUST pass it, and the pureredactSecrets(schema, value)walker is exported for other wires.get(ns)— resolved value,undefinedwhile unregistered.update(ns, patch)— deep-merges the plain-object patch into the user section only (never thebase), validates the resolved candidate, persists through the provider, then commits. Validation failure rejects before anything is persisted; a read-only provider (writable: false) rejects every write. Writes to one namespace are serialized in call order.replace(ns, section)— sets the user section wholesale: the removal/reset path a merge cannot express (replace({})re-inheritsbaseand schema defaults).- Resolved values are deep-frozen snapshots. Watchers receive
(next, prev)after each commit: invocations of one callback run asynchronously, one at a time, in commit order (a slow stale invocation can never apply after a newer one), and failures — sync throws and async rejections alike — are contained. Thesettings/updatedevent fans out one listener at a time, so one throwing listener cannot starve the rest. - Service teardown refuses new writes and drains every queued write before disposal completes; a write whose registrant fiber was disposed mid-flight still reaches storage but commits and notifies nobody.
Provider contract
Subclasses implement writable, load(), and persist(ns, section), and push externally observed documents through the protected publish(doc). The base service init loads and publishes the document once before the service becomes injectable; a provider with its own init (watcher, connection) delegates first via yield* super[Service.init](). At publish, each registered namespace re-resolves independently: an invalid section keeps that namespace's last good value and warns — a live reload never takes the process down — while boot-time and registration-time validation fail loud.
Events
settings/updated (ns, next, prev, source) fires after each commit; source is update (in-process write) or provider (external change). It never fires for a deep-equal resolved value.
Model Experience
Indirectly, through consumer plugins that resolve model-affecting values (for example a default model route) from their namespaces; each consumer's own surface documents the effect.
KV Cache effect
No direct invalidation; a consumer that folds a settings value into the request prefix owns that change.
Known Limitations and Deferred Work
- Single user layer — resolution knows schema defaults, one composition
base, and one user document; there is no project/managed layering or per-value provenance yet. - Cross-process concurrency is provider-defined — the seam serializes writes per namespace in-process only; concurrent processes converge by provider behavior (the local file provider is last-write-wins).