/** * config-ownership.ts, which runtime OWNS a config key. * * Ownership follows the runtime that ACTS on a setting, not the client that * happens to edit it. Before this module existed, every product wrote every key * into its own surface silo (`~/.goodvibes/agent/settings.json`, * `~/.goodvibes/tui/settings.json`, ...). The daemon reads exactly one of those * files, so a Telegram bot username set from the agent reported success, landed * in the agent's file, and configured nothing: Telegram runs in the daemon. * * Three scopes: * * - `daemon`, the daemon executes it unattended, so it has exactly one home: * the daemon tier (`~/.goodvibes/daemon/settings.json`). Chat surfaces, * control-plane binding, watchers and triggers, device pairing and grants, * local voice provisioning, delivery, at-rest retention. * * - `client`, presentation and per-installation lifecycle. Genuinely local and * genuinely different between the TUI, the agent and the web UI: rendering, * theme, transcript display, keybindings, and the "do I run/embed a daemon at * all" switches (`daemon.*`, `service.*`) which are a property of THIS * installation, not of the daemon's behavior. This is the DEFAULT scope, a * key is client-owned unless it is listed below, so adding a schema key never * silently relocates a user's existing value. * * - `user`, cross-client defaults that ride the surface-root-independent * shared tier (`~/.goodvibes/shared/settings.json`). * * Two user-level precedences exist and the difference is deliberate: * - `shared-wins` (the voice/tts keys, unchanged since the shared tier * shipped): the shared value overlays the surface value, so every surface * speaks with one voice. * - `local-override` (model / reasoning effort): the shared value is a * DEFAULT that applies only where the surface has not set the key itself, * which is what "a client may override locally" means. */ import type { ConfigKey } from './schema.js'; /** Which runtime owns, and therefore writes, a config key. */ export type ConfigScope = 'daemon' | 'client' | 'user'; /** * Whole config domains the daemon executes unattended. A key is daemon-owned * when it starts with one of these prefixes. * * Deliberately NOT here, and why: * - `daemon.*` , "does THIS installation run/embed a daemon"; the agent * answers no and the TUI answers yes, and neither answer is * the daemon's to give. Making it daemon-owned would make * the agent start a daemon because the TUI runs one. * - `service.*` , same shape: per-installation platform-service lifecycle. * - `voice.wake.*`, the wake word listens inside each client process. */ export declare const DAEMON_OWNED_CONFIG_PREFIXES: readonly string[]; /** Individual daemon-owned keys that do not sit under a daemon-owned domain. */ export declare const DAEMON_OWNED_CONFIG_KEYS: readonly string[]; /** * Daemon-owned config paths that are NOT scalar schema keys. * * `listDaemonOwnedConfigKeys()` is derived from CONFIG_SCHEMA, and CONFIG_SCHEMA * only describes scalars. An array-valued daemon setting therefore has real * ownership (`isDaemonOwnedConfigKey` matches it by prefix, so `set` routes it * to the daemon store) but was invisible to everything that WALKS the owned set: * the migration never moved it, the daemon-tier overlay never read it back, and * a whole-config save never stripped it from the surface file. The result is a * value that looks daemon-owned, is written to the daemon store when set through * the API, and is silently ignored when it already exists in a client silo. * * `conversationGate.gatedSurfaces` is the live case: `conversationGate.mode` * migrated and this list did not, so a machine could have the gate's mode in the * daemon store and its surface list stranded in `~/.goodvibes/tui/settings.json`. * * Adding a path here grows the covered set, which is what makes the migration * re-run on the next start and pick the stranded value up. */ export declare const DAEMON_OWNED_NON_SCHEMA_CONFIG_PATHS: readonly ["conversationGate.gatedSurfaces", "cluster.peers", "cluster.groupMaterial"]; /** A daemon-owned path that has no scalar CONFIG_SCHEMA entry. */ export type DaemonOwnedNonSchemaConfigPath = (typeof DAEMON_OWNED_NON_SCHEMA_CONFIG_PATHS)[number]; /** Any path the daemon owns: a schema key, or a non-scalar path listed above. */ export type DaemonOwnedConfigPath = ConfigKey | DaemonOwnedNonSchemaConfigPath; /** * User-level keys whose shared value OVERLAYS the surface value. This is the * original shared tier (see shared-config-tier.ts) and its behavior is * unchanged: one voice on every surface. */ export declare const USER_SHARED_WINS_CONFIG_KEYS: readonly string[]; /** * User-level keys whose shared value is only a DEFAULT: a surface that carries * its own explicit value keeps it. Set through `ConfigManager.setUserDefault`; * an ordinary `set` on one of these writes the surface-local override, which is * the behavior every existing installation already has. */ export declare const USER_LOCAL_OVERRIDE_CONFIG_KEYS: readonly string[]; /** * True when the daemon is the single writer and reader-of-record for `key`. * * The non-schema list is consulted HERE and not only by the owned-set walk. * When that list held nothing but `conversationGate.gatedSurfaces` and * `cluster.peers` the distinction did not matter: both sit under a daemon-owned * PREFIX, so this predicate already answered yes and the list existed purely so * a walk over owned paths would not miss a non-scalar. `email.passwordRef`, the * calendar client secrets and the Google refresh token went through exactly * that gap for a while: they were on this list before `email.`/`calendar.`/ * `google.` were daemon-owned prefixes, so the two answers disagreed, the * walk called them daemon-owned while this predicate called them client-owned. * They are schema keys under those prefixes now (schema-domain-connectors.ts), * so the prefix match alone answers yes for all of them. * * A key nobody claims is not stored twice, it is stored NOWHERE. The manager * routes daemon-owned keys to the daemon tier and everything else to the * surface tier, and a dynamic key that failed both tests was accepted, reported * as saved, and written to neither file. That is the exact silence config * ownership exists to prevent, applied to a password reference. */ export declare function isDaemonOwnedConfigKey(key: string): boolean; /** True when `key` rides the cross-client user tier (either precedence). */ export declare function isUserLevelConfigKey(key: string): boolean; /** True when a user-level key's shared value overlays the surface value. */ export declare function userTierOverlaysSurface(key: string): boolean; /** True when `key` is presentation/per-installation state owned by each client. */ export declare function isClientOwnedConfigKey(key: string): boolean; /** * The owning runtime for `key`. `client` is the default, so an unclassified or * brand-new key keeps writing to the surface silo it already writes to. */ export declare function configKeyScope(key: string): ConfigScope; /** * Every schema key the daemon owns, in schema order. Memoized: the load and * migration paths walk this list per settings file, and CONFIG_SCHEMA is a * frozen module constant. */ export declare function listDaemonOwnedConfigKeys(): readonly ConfigKey[]; /** * Every path the daemon owns, schema keys PLUS the non-scalar paths above. * * This is the list every owned-set walk should use: migration, the daemon-tier * overlay, whole-config strip, and reset. `listDaemonOwnedConfigKeys` remains * for callers that genuinely need schema keys only (typed `get`/`set` surfaces). */ export declare function listDaemonOwnedConfigPaths(): readonly DaemonOwnedConfigPath[]; /** * Human-readable reason a client may not be the writer for `key`, used by the * routing layer's failure text so "this went somewhere else" is never silent. */ export declare function describeConfigOwnership(key: string): string; //# sourceMappingURL=config-ownership.d.ts.map