Skip to main content
PathDocs

Settings System

Short version: ctx.settings is the runtime user-config service: what a user changes in the UI/Settings is stored into ~/.dsh/settings.yaml (hot-published by the dsh-settings-file file provider), and the settings/document-updated event keeps the UI and plugins in sync.

This section explains "that layer beyond patch's static configuration, changeable by the user at runtime".

1. Which layer is this

patch layersettings layer
Who writesDeployer (cordis.patch.yml)User (UI / file)
ContentStatic config / mountingRuntime user settings
Carriercordis.patch.yml~/.dsh/settings.yaml
ChangeEdit file, restartHot-published, settings/document-updated

Patch and settings are two configuration seams, complementary (see Configuration).

2. File provider: settings-file

dsh-settings-file is the default implementation, carrying each namespace's section in a single YAML/JSON document:

FieldDefaultDescription
pathsettings.yaml (under harness home)Document path; the extension picks the format (.yaml/.yml/.json)
dshHome$DSH_HOME or ~/.dshHarness home when path is omitted
watchtrueWatches the document, hot-publishing external edits
debounceMs100Write settle window (ms)

Behavior notes:

  • fail-loud at startup, last-good on reload: an existing invalid document → the plugin fails to load; an unreadable live edit → warns and keeps the last good section; a missing document → each namespace resolves from defaults+base
  • Every write is read-modify-write: re-reads the document, publishes the diff, then writes: never revives a stale document or drops unobserved sibling sections
  • Writes hold a cross-process write lock (*.lock sibling, wx creation, 2s acquisition cap)
  • Atomic, owner-only, symlink-resistant: rendering uses a random-suffix temp (0600) then renames over
  • YAML edits are leaf-level diffs: only change the altered values / remove dropped keys, preserving comment anchors and formatting
  • documentPath is a resolved absolute path: ctx.settings.documentPath is the resolveSpec() filename; the browser only gets an availability flag, never rebuilds $DSH_HOME, never commits a filesystem destination
  • Its own writes are deduplicated by content: the provider caches the last good text; when a watcher event's content equals the cache (including its own writes) it's a no-op
  • No indirection for values: sections store literal values; ${env:VAR}-style key references are a deferred seam-layer feature

3. Surface: how plugins use it

import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import z from '@deepseek-ai/schemastery'
import { Context } from '@deepseek-ai/cordis'

const ns = settingsNamespace('my-plugin')
const schema = z.object({
greeting: z.string().default('hello'),
maxItems: z.number().step(1).min(1).default(10),
})

export function apply(ctx: Context) {
const scope = ctx.settings.register(ns, schema, { /* base? */ })
// resolution order: schema defaults → composition base → user layer
// scope.get() / scope.update({...}) / scope.watch(...)
}
  • register(ns, schema, {base?}) returns the owner SettingsScope (get/watch/update)
  • Registration is an effect: disposing the fiber removes the namespace and observers
  • A stored section rejected by the schema → the registration fails; a duplicate namespace → fails loud
  • A full example is in Configuration & Publishing, "Runtime user config"

4. Built-in namespace: agent-default-model

agent-default-model is a settings section DSH ships, provided by the dsh-agent-default-model service (ctx.agentDefaultModel):

  • currentSelection() returns a detached { provider, model, reasoningEffort? } selection for newly-created agents
  • saveSelection(selection) saves the full user selection; it's a no-op when there's no settings provider, so the deployment default in the composition stays in effect
  • The deployment default is written into the plugin config (required { provider, model }) as the base of the settings section; once a settings provider is mounted, the user selection overrides on top, taking effect on the next currentSelection() read
  • reasoningEffort belongs only to the settings section, never the plugin config: a fully-saved selection can clear it when the next model has no effort, while a composition value would be inherited again
  • The service does not validate directory membership: a provider route may serve an un-registered model; the consumer that actually opens the model request is responsible for availability diagnostics
# ~/.dsh/settings.yaml
agent-default-model:
provider: deepseek-official
model: deepseek-v4-flash

Direct entry points (e.g. dsh --profile headless) and Host-side entry points (e.g. ApiProxy) read the same service, not maintaining separate provider/model defaults.

5. Events

  • settings/document-updated: fired when ctx.settings values change
  • Third-party plugins/UI listen to it to refresh; it also triggers credentials/updated (if it contains credential references)

6. Verification

# Inspect the settings document (the layer a user can change)
cat ~/.dsh/settings.yaml
# Check the settings service in the composition
dsh web --dump-config | grep -i settings

Next steps