mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
The remaining P1 from the #939 review, plus the P2 it shares a mechanism with. Nothing carried a version, so two tabs editing one namespace silently overwrote each other — reproduced as tab B's `reasoning` lost to tab A's older draft. The seam's per-namespace write queue orders writes; it cannot tell a fresh writer from one replaying a snapshot a predecessor superseded. Each namespace now carries a monotonic `revision` over its RAW section. A write may send `expectedRevision`, checked at the FRONT of the queue (not at call time, which would race the very predecessor it guards against); a mismatch rejects with `SettingsConflictError` → `settings-conflict` on the wire, carrying both revisions. The editor captures the revision it opened at and, on conflict, asks the user to reopen rather than replaying its snapshot. The same counter fixes the missing broadcast. `settings/updated` is gated on the resolved value — correct for consumers, wrong for configuration surfaces: storing an override equal to the composition base leaves the resolved value alone while changing what the document says (the field is now overridden, not inherited) and moving every open editor's revision. `settings/document-updated (ns, revision)` fires on any raw-section change, in-process or external, and `host/settings-changed` now rides it. That event also closes the stale model picker: editing a provider's `models` changes no route, so `llm/adapters-updated` never fired and an open picker kept serving the old catalog. A change to an exposed provider namespace now emits `host/models-changed` too — that namespace holds the catalog. Docs: both sides of the five touched README pairs, a type-equiv block for `SettingsPathOp`, and an Agent Note recording what the plane exposes and who may overwrite what. The deferred wire-redaction gaps (secrets behind union/intersection/transform, `.default(...)` in the served envelope, schema text in rejection messages, `new Function` rehydration, pi-ai's `headers`) are recorded as TODO(settings-wire-redaction) and in Known Limitations rather than half-fixed.
91 lines
4.0 KiB
TypeScript
91 lines
4.0 KiB
TypeScript
/**
|
|
* settings domain contract: the web face of the user-settings seam
|
|
* (`ctx.settings`). Every payload that leaves this domain is redacted by the
|
|
* seam (`describe({ redactSecrets: true })` semantics): `role('secret')`
|
|
* fields never ride a response in any layer, and the `secrets` slot list is
|
|
* how a form learns a write-only field exists and whether it is configured.
|
|
*/
|
|
|
|
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
|
|
|
/** One schema-declared secret slot inside a redacted namespace value. */
|
|
export interface SettingsSecretView {
|
|
/** Path from the section root to the removed field. */
|
|
path: string[]
|
|
/** Whether the slot currently holds a value (the value itself never rides). */
|
|
set: boolean
|
|
}
|
|
|
|
/** Wire view of one registered settings namespace. */
|
|
export interface SettingsNamespaceView {
|
|
/** Namespace key (`llm-deepseek`, `llm-pi-ai`, …). */
|
|
ns: string
|
|
/** Serialized schemastery schema envelope (`schema.toJSON()`); rehydrate with `new Schema(json)`. */
|
|
schema: unknown
|
|
/** Redacted resolved value (schema defaults → composition base → user layer). */
|
|
value: unknown
|
|
/** Redacted composition base layer, when the registrant declared one. */
|
|
base?: unknown
|
|
/** Redacted raw user section, when one exists; a field's presence here marks it user-overridden. */
|
|
user?: unknown
|
|
/** When the owner applies changes. */
|
|
applies: 'live' | 'restart'
|
|
/** Every schema-declared secret slot with its configured state. */
|
|
secrets: SettingsSecretView[]
|
|
/**
|
|
* Monotonic revision of the raw user section this view was read at. Send it
|
|
* back as `expectedRevision` on a write so a stale editor is refused rather
|
|
* than silently overwriting a concurrent change.
|
|
*/
|
|
revision: number
|
|
}
|
|
|
|
/**
|
|
* One path-addressed edit carried by `settings.mutate`. `set` writes the
|
|
* value at the path (creating intermediate objects); `unset` removes it. The
|
|
* empty path addresses the section root.
|
|
*/
|
|
export type SettingsPathOpView =
|
|
| { op: 'set'; path: string[]; value: unknown }
|
|
| { op: 'unset'; path: string[] }
|
|
|
|
/** Settings-domain unary methods (the map keys settings.* of RpcMethodMap). */
|
|
export interface SettingsApi {
|
|
/**
|
|
* Describe every registered namespace: redacted layered values plus the
|
|
* serialized schema a client renders its form from. `writable: false`
|
|
* (read-only provider) tells the client to disable every write control.
|
|
*/
|
|
describe(request: RpcRequest<{}>): Promise<RpcResponse<{ writable: boolean; namespaces: SettingsNamespaceView[] }>>
|
|
|
|
/**
|
|
* Merge a patch into one namespace's user layer (validate → persist →
|
|
* commit). Secret-role fields may be INCLUDED in the patch (write-only
|
|
* direction); a form that leaves a secret untouched simply omits it and the
|
|
* merge preserves the stored value. Responds with the namespace's new
|
|
* redacted view; a schema or storage rejection is `settings-rejected`.
|
|
*/
|
|
update(request: RpcRequest<{ ns: string; patch: object; expectedRevision?: number }>): Promise<RpcResponse<SettingsNamespaceView>>
|
|
|
|
/**
|
|
* Replace one namespace's user section wholesale — the removal/reset path a
|
|
* merge cannot express (`section: {}` resets to composition defaults). Keys
|
|
* absent from `section` are dropped, secrets included: a client must first
|
|
* fold the descriptor's `user` layer (and re-supply any secret it wants to
|
|
* keep) or accept the reset.
|
|
*/
|
|
replace(request: RpcRequest<{ ns: string; section: object; expectedRevision?: number }>): Promise<RpcResponse<SettingsNamespaceView>>
|
|
|
|
/**
|
|
* Apply path-addressed edits to one namespace's user section, resolved
|
|
* against the section as stored — NOT against whatever the caller last
|
|
* read. This is the removal path for any client holding the redacted
|
|
* descriptor: it names the field it means, so a secret the wire never
|
|
* returned cannot be deleted as a side effect. `replace` remains the
|
|
* deliberate wholesale reset.
|
|
*/
|
|
mutate(
|
|
request: RpcRequest<{ ns: string; ops: SettingsPathOpView[]; expectedRevision?: number }>,
|
|
): Promise<RpcResponse<SettingsNamespaceView>>
|
|
}
|