Kward

Module: Kward::ConfigFiles

Defined in:
lib/kward/config_files.rb

Overview

Resolves Kward configuration, cache, memory, prompt, skill, and plugin paths, and reads/writes the JSON config file used by the CLI and RPC server.

This module is the configuration boundary, not a runtime settings cache. Most methods read the filesystem each time so CLI commands and RPC reloads can observe edits made outside the process. Callers that need caching should own invalidation explicitly, as Client#reload_config does for provider state.

Keep path decisions here. Higher-level code should ask ConfigFiles for config, prompt, skill, plugin, cache, memory, and session locations instead of reconstructing ~/.kward paths independently.

Defined Under Namespace

Classes: PromptTemplate, Skill

Constant Summary collapse

MAX_SKILL_FILE_BYTES =
100_000
MAX_PROMPT_FILE_BYTES =
32 * 1024
DEFAULT_OVERLAY_SETTINGS =
{ "alignment" => "center", "width" => "maximum" }.freeze
DEFAULT_PERSONAS =
{
  "characters" => [
    {
      "key" => "kward",
      "label" => "Kward",
      "instruction" => "Your name is Kward, the grim Andruid - robotic keeper of the Forrest of Code, protecting the nature of good engineering priciples. Speak like an old druid, be suspicous of everyone, but with a good intend."
    }
  ],
  "default" => "kward"
}.freeze
OVERLAY_ALIGNMENTS =
%w[left center right].freeze
OVERLAY_WIDTHS =
%w[capped maximum].freeze

Class Method Summary collapse

Class Method Details

.active_persona_label(workspace_root:, model: nil, config: read_config) ⇒ Object

Returns the label of the persona selected by default/workspace/model rules.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/kward/config_files.rb', line 290

def active_persona_label(workspace_root:, model: nil, config: read_config)
  personas = config["personas"]
  return nil unless personas.is_a?(Hash)

  labels = crew_character_labels(personas)
  active_label = persona_label_for_key(personas["default"], labels) unless personas["default"].nil?

  workspaces = personas["workspaces"]
  if workspaces.is_a?(Hash)
    root = canonical_workspace_root(workspace_root)
    workspaces.each do |path, key|
      next unless canonical_workspace_root(path) == root

      active_label = persona_label_for_key(key, labels)
      break
    end
  end

  models = personas["models"]
  if models.is_a?(Hash) && !model.to_s.empty? && models.key?(model.to_s)
    active_label = persona_label_for_key(models[model.to_s], labels)
  end

  active_label
end

.add_persona_entry(entries, layer, value, name: nil) ⇒ Object



400
401
402
403
404
405
# File 'lib/kward/config_files.rb', line 400

def add_persona_entry(entries, layer, value, name: nil)
  text = presence(value)
  return unless text

  entries << { layer: layer.to_s, name: name.to_s, prompt: text }
end

.agents_promptString?

Reads global principle instructions from the config directory.

PRINCIPLES.md is preferred. AGENTS.md remains a backwards-compatible alias for existing installations.

Returns:

  • (String, nil)

    prompt text, or nil when absent/too large



252
253
254
255
256
257
# File 'lib/kward/config_files.rb', line 252

def agents_prompt
  path = config_principles_path
  return read_prompt_file(path, "Kward principles file") if File.exist?(path)

  read_prompt_file(config_agents_path, "Kward AGENTS.md alias")
end

Returns whether the terminal startup banner should be displayed.

Returns:

  • (Boolean)


193
194
195
196
# File 'lib/kward/config_files.rb', line 193

def banner_enabled?(config = read_config)
  banner = config["banner"].is_a?(Hash) ? config["banner"] : {}
  banner["enabled"] != false
end

.cache_dirObject



63
64
65
# File 'lib/kward/config_files.rb', line 63

def cache_dir
  File.join(config_dir, "cache")
end

.canonical_workspace_root(path) ⇒ Object



395
396
397
398
# File 'lib/kward/config_files.rb', line 395

def canonical_workspace_root(path)
  expanded = File.expand_path(path.to_s.empty? ? Dir.pwd : path.to_s)
  File.directory?(expanded) ? File.realpath(expanded) : expanded
end

.character_entries(raw) ⇒ Object



447
448
449
450
451
452
453
454
455
456
# File 'lib/kward/config_files.rb', line 447

def character_entries(raw)
  case raw
  when Hash
    raw.map { |key, definition| [key, definition] }
  when Array
    raw.filter_map { |entry| character_entry(entry) }
  else
    []
  end
end

.character_entry(entry) ⇒ Object



458
459
460
461
462
463
464
465
466
467
# File 'lib/kward/config_files.rb', line 458

def character_entry(entry)
  return nil unless entry.is_a?(Hash)

  if entry.length == 1 && entry.keys.first.is_a?(String)
    [entry.keys.first, entry.values.first]
  else
    key = entry["key"] || entry[:key] || entry["id"] || entry[:id] || entry["name"] || entry[:name]
    key.to_s.empty? ? nil : [key, entry]
  end
end

.code_search_cache_dirObject



96
97
98
# File 'lib/kward/config_files.rb', line 96

def code_search_cache_dir
  File.join(cache_dir, "code_search")
end

.composer_busy_help?(config = read_config) ⇒ Boolean

Returns whether the composer should show busy-state keyboard help.

Returns:

  • (Boolean)


187
188
189
190
# File 'lib/kward/config_files.rb', line 187

def composer_busy_help?(config = read_config)
  composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
  composer["busy_help"] != false
end

.config_agents_pathObject



263
264
265
# File 'lib/kward/config_files.rb', line 263

def config_agents_path
  File.join(config_dir, "AGENTS.md")
end

.config_dirString

Directory that contains Kward's user config and adjacent prompt/skill data. Defaults to ~/.kward, or the directory of KWARD_CONFIG_PATH.

Returns:

  • (String)

    expanded config directory path



51
52
53
54
55
56
# File 'lib/kward/config_files.rb', line 51

def config_dir
  config_path = ENV["KWARD_CONFIG_PATH"]
  return File.expand_path(File.dirname(config_path)) if config_path && !config_path.empty?

  File.expand_path("~/.kward")
end

.config_pathString

Returns expanded JSON config file path.

Returns:

  • (String)

    expanded JSON config file path



59
60
61
# File 'lib/kward/config_files.rb', line 59

def config_path
  File.expand_path(ENV["KWARD_CONFIG_PATH"] || File.join(config_dir, "config.json"))
end

.config_principles_pathObject



259
260
261
# File 'lib/kward/config_files.rb', line 259

def config_principles_path
  File.join(config_dir, "PRINCIPLES.md")
end

.config_value(config, *keys) ⇒ Object

Returns the first present non-empty string value among several config keys.



163
164
165
166
167
168
169
# File 'lib/kward/config_files.rb', line 163

def config_value(config, *keys)
  keys.each do |key|
    text = presence(config[key])
    return text if text
  end
  nil
end

.crew_character_labels(personas) ⇒ Object



413
414
415
416
417
# File 'lib/kward/config_files.rb', line 413

def crew_character_labels(personas)
  named_character_values(personas) do |_key, definition|
    extract_character_label(definition)
  end
end

.crew_characters(personas) ⇒ Object



407
408
409
410
411
# File 'lib/kward/config_files.rb', line 407

def crew_characters(personas)
  named_character_values(personas) do |_key, definition|
    extract_character_instruction(definition)
  end
end

.default_configObject



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/kward/config_files.rb', line 67

def default_config
  {
    "personas" => JSON.parse(JSON.generate(DEFAULT_PERSONAS)),
    "memory" => {
      "enabled" => false,
      "auto_summary" => false
    },
    "composer" => {
      "busy_help" => true
    },
    "sessions" => {
      "auto_resume" => false
    },
    "enforce_workspace_agents_file" => false,
    "tools" => {
      "workspace_guardrails" => true
    }
  }
end

.delete_config_key(key, path = config_path) ⇒ Object

Removes a top-level config key when it exists.



154
155
156
157
158
159
160
# File 'lib/kward/config_files.rb', line 154

def delete_config_key(key, path = config_path)
  config = read_config(path)
  existed = config.key?(key.to_s)
  config.delete(key.to_s)
  write_config(config, path) if existed
  existed
end

.enforce_workspace_agents_file?(config = read_config) ⇒ Boolean

Returns whether workspace AGENTS.md contents should be injected directly instead of a compact read-when-relevant instruction.

Returns:

  • (Boolean)


212
213
214
# File 'lib/kward/config_files.rb', line 212

def enforce_workspace_agents_file?(config = read_config)
  config["enforce_workspace_agents_file"] == true
end

.ensure_default_config!(path = config_path) ⇒ Object

Performs ensure default config for configuration file and path handling.



88
89
90
91
92
93
94
# File 'lib/kward/config_files.rb', line 88

def ensure_default_config!(path = config_path)
  path = File.expand_path(path)
  return false if File.exist?(path)

  write_config(default_config, path)
  true
end

.extract_character_instruction(definition) ⇒ Object



475
476
477
478
479
480
481
482
483
484
# File 'lib/kward/config_files.rb', line 475

def extract_character_instruction(definition)
  return nil if definition.nil?

  if definition.is_a?(Hash)
    value = definition["instruction"] || definition[:instruction]
    return presence(value)
  end

  presence(definition)
end

.extract_character_label(definition) ⇒ Object



469
470
471
472
473
# File 'lib/kward/config_files.rb', line 469

def extract_character_label(definition)
  return nil unless definition.is_a?(Hash)

  presence(definition["label"] || definition[:label])
end

.inside_directory?(path, base) ⇒ Boolean

Returns:

  • (Boolean)


575
576
577
# File 'lib/kward/config_files.rb', line 575

def inside_directory?(path, base)
  path == base || path.start_with?(base + File::SEPARATOR)
end

.markdown_parts(path) ⇒ Object



562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/kward/config_files.rb', line 562

def markdown_parts(path)
  content = File.read(path)
  return [{}, content] unless content.start_with?("---\n", "---\r\n")

  _opening, rest = content.split(/\A---\r?\n/, 2)
  yaml_text, body = rest.to_s.split(/\r?\n---\r?\n/, 2)
  raise "missing frontmatter closing delimiter" if body.nil?

  data = yaml_text.to_s.empty? ? {} : YAML.safe_load(yaml_text, permitted_classes: [], aliases: false)
  frontmatter = data.is_a?(Hash) ? data.transform_keys(&:to_s) : {}
  [frontmatter, body]
end

.memory_core_pathObject



105
106
107
# File 'lib/kward/config_files.rb', line 105

def memory_core_path
  File.join(memory_dir, "core.json")
end

.memory_dirString

Returns directory containing structured memory files.

Returns:

  • (String)

    directory containing structured memory files



101
102
103
# File 'lib/kward/config_files.rb', line 101

def memory_dir
  File.join(config_dir, "memory")
end

.memory_events_pathObject



113
114
115
# File 'lib/kward/config_files.rb', line 113

def memory_events_path
  File.join(memory_dir, "events.jsonl")
end

.memory_soft_pathObject



109
110
111
# File 'lib/kward/config_files.rb', line 109

def memory_soft_path
  File.join(memory_dir, "soft.jsonl")
end

.named_character_values(personas) ⇒ Object



438
439
440
441
442
443
444
445
# File 'lib/kward/config_files.rb', line 438

def named_character_values(personas)
  character_entries(personas["characters"] || personas["crew"]).each_with_object({}) do |(key, definition), mapping|
    value = yield(key, definition)
    next if value.to_s.empty?

    mapping[key.to_s] = value
  end
end

.overlay_settings(config = read_config) ⇒ Hash

Returns validated overlay settings with defaults for missing or invalid values.

Parameters:

  • config (Hash) (defaults to: read_config)

    parsed config object

Returns:

  • (Hash)

    overlay settings with alignment and width



176
177
178
179
180
181
182
183
184
# File 'lib/kward/config_files.rb', line 176

def overlay_settings(config = read_config)
  overlay = config["overlay"].is_a?(Hash) ? config["overlay"] : {}
  settings = DEFAULT_OVERLAY_SETTINGS.dup
  alignment = overlay["alignment"].to_s
  width = overlay["width"].to_s
  settings["alignment"] = alignment if OVERLAY_ALIGNMENTS.include?(alignment)
  settings["width"] = width if OVERLAY_WIDTHS.include?(width)
  settings
end

.persona_entries(workspace_root:, model: nil, reasoning_effort: nil, now: Time.now, config: read_config, include_reasoning: true) ⇒ Object



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/kward/config_files.rb', line 316

def persona_entries(workspace_root:, model: nil, reasoning_effort: nil, now: Time.now, config: read_config, include_reasoning: true)
  personas = config["personas"]
  return [] unless personas.is_a?(Hash)

  characters = crew_characters(personas)
  entries = []
  active_persona = { layer: "default", value: personas["default"], name: nil }

  workspaces = personas["workspaces"]
  if workspaces.is_a?(Hash)
    root = canonical_workspace_root(workspace_root)
    workspaces.each do |path, key|
      if canonical_workspace_root(path) == root
        active_persona = { layer: "workspace", value: key, name: path }
        break
      end
    end
  end

  models = personas["models"]
  if models.is_a?(Hash) && !model.to_s.empty? && models.key?(model.to_s)
    active_persona = { layer: "model", value: models[model.to_s], name: model.to_s }
  end

  add_persona_entry(
    entries,
    active_persona.fetch(:layer),
    resolved_persona_text(active_persona.fetch(:value), characters: characters),
    name: active_persona[:name]
  )

  modifiers = personas["persona_modifiers"]
  if modifiers.is_a?(Hash)
    if include_reasoning
      reasoning = modifiers["reasoning"]
      add_persona_entry(entries, "reasoning", reasoning[reasoning_effort.to_s]) if reasoning.is_a?(Hash) && !reasoning_effort.to_s.empty?
    end

    time_of_day = modifiers["time_of_day"]
    bucket = time_of_day_bucket(now)
    add_persona_entry(entries, "time_of_day", time_of_day[bucket], name: bucket) if time_of_day.is_a?(Hash)

    weekday = modifiers["weekday"]
    day = weekday_name(now)
    add_persona_entry(entries, "weekday", weekday[day], name: day) if weekday.is_a?(Hash)

    add_persona_entry(entries, "suffix", modifiers["suffix"])
  end

  entries
end

.persona_label_for_key(value, labels) ⇒ Object



431
432
433
434
435
436
# File 'lib/kward/config_files.rb', line 431

def persona_label_for_key(value, labels)
  key = value.to_s.strip
  return nil if key.empty?

  presence(labels[key])
end

.persona_prompt(workspace_root, model: nil, reasoning_effort: nil, now: Time.now, config: read_config) ⇒ String?

Builds persona prompt text from default, workspace, model, reasoning, time-of-day, weekday, and suffix config entries.

Persona resolution is intentionally data-driven so users can edit config without plugin code. Keep new persona selectors additive and deterministic; prompt construction depends on stable ordering.

Parameters:

  • workspace_root (String)

    active workspace root

  • model (String, nil) (defaults to: nil)

    active model name

  • reasoning_effort (String, nil) (defaults to: nil)

    active reasoning effort

  • now (Time) (defaults to: Time.now)

    local time used for time-based modifiers

  • config (Hash) (defaults to: read_config)

    parsed config object

Returns:

  • (String, nil)

    persona prompt text when entries match



280
281
282
283
284
285
286
287
# File 'lib/kward/config_files.rb', line 280

def persona_prompt(workspace_root, model: nil, reasoning_effort: nil, now: Time.now, config: read_config)
  text = persona_entries(workspace_root: workspace_root, model: model, reasoning_effort: reasoning_effort, now: now, config: config).map do |entry|
    entry[:prompt]
  end.join("\n\n")
  return nil if text.empty?

  text
end

.plugin_dirString

Returns trusted user plugin directory.

Returns:

  • (String)

    trusted user plugin directory



507
508
509
# File 'lib/kward/config_files.rb', line 507

def plugin_dir
  File.expand_path("~/.kward/plugins")
end

.plugin_pathsArray<String>

Finds trusted top-level plugin files.

Plugins are intentionally loaded only from ~/.kward/plugins, not from a workspace or custom KWARD_CONFIG_PATH directory.

Returns:

  • (Array<String>)

    sorted plugin file paths



517
518
519
520
521
522
523
524
525
# File 'lib/kward/config_files.rb', line 517

def plugin_paths
  plugins_root = plugin_dir
  return [] unless Dir.exist?(plugins_root)

  Dir.glob(File.join(plugins_root, "*.rb")).sort
rescue StandardError => e
  warn "Warning: skipping Kward plugins in #{plugins_root}: #{e.message}"
  []
end

.presence(value) ⇒ Object



579
580
581
582
# File 'lib/kward/config_files.rb', line 579

def presence(value)
  text = value.to_s
  text.empty? ? nil : text
end

.prompt_template_registryObject



554
555
556
557
558
559
560
# File 'lib/kward/config_files.rb', line 554

def prompt_template_registry
  Prompts::Templates.new(
    config_dir: config_dir,
    template_class: PromptTemplate,
    markdown_parser: method(:markdown_parts)
  )
end

.prompt_templates(reserved_commands: []) ⇒ Array<PromptTemplate>

Lists prompt templates exposed as slash commands.

Parameters:

  • reserved_commands (Array<String>) (defaults to: [])

    command names unavailable to templates

Returns:



531
532
533
# File 'lib/kward/config_files.rb', line 531

def prompt_templates(reserved_commands: [])
  prompt_template_registry.prompt_templates(reserved_commands: reserved_commands)
end

.read_config(path = config_path) ⇒ Hash

Reads the JSON config file.

Missing files are treated as an empty config. Invalid JSON raises a user-facing error that includes the file path. This method does not merge defaults; callers should apply feature-specific defaults at the point where behavior is decided.

Parameters:

  • path (String) (defaults to: config_path)

    config file path

Returns:

  • (Hash)

    parsed config object



126
127
128
129
130
131
132
133
# File 'lib/kward/config_files.rb', line 126

def read_config(path = config_path)
  path = File.expand_path(path)
  return {} unless File.exist?(path)

  JSON.parse(File.read(path))
rescue JSON::ParserError
  raise "Invalid Kward config JSON: #{path}"
end

.read_prompt_file(path, label) ⇒ Object



380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/kward/config_files.rb', line 380

def read_prompt_file(path, label)
  return nil unless File.exist?(path)

  size = File.size(path)
  if size > MAX_PROMPT_FILE_BYTES
    warn "Warning: skipping #{label} #{path}: file too large (#{size} bytes; limit is #{MAX_PROMPT_FILE_BYTES} bytes)"
    return nil
  end

  File.read(path)
rescue StandardError => e
  warn "Warning: skipping #{label} #{path}: #{e.message}"
  nil
end

.read_skill_file(name, relative_path = nil) ⇒ String

Reads a skill file by skill name and optional relative path.

Parameters:

  • name (String)

    configured skill name

  • relative_path (String, nil) (defaults to: nil)

    path inside the skill directory

Returns:

  • (String)

    file contents or an error string



540
541
542
# File 'lib/kward/config_files.rb', line 540

def read_skill_file(name, relative_path = nil)
  skills_registry.read_skill_file(name, relative_path)
end

.resolved_persona_text(value, characters: {}) ⇒ Object



419
420
421
422
423
424
425
426
427
428
429
# File 'lib/kward/config_files.rb', line 419

def resolved_persona_text(value, characters: {})
  return nil if value.nil?

  key = value.to_s.strip
  return nil if key.empty?

  text = characters[key.to_s]
  return text unless text.to_s.empty?

  value
end

.session_auto_resume_enabled?(config = read_config) ⇒ Boolean

Returns whether new frontends should resume the last active session automatically.

Returns:

  • (Boolean)


205
206
207
208
# File 'lib/kward/config_files.rb', line 205

def session_auto_resume_enabled?(config = read_config)
  sessions = config["sessions"].is_a?(Hash) ? config["sessions"] : {}
  sessions["auto_resume"] == true
end

.skillsArray<Skill>

Lists configured skills discovered under the config directory.

Returns:

  • (Array<Skill>)

    skill metadata available to the model



502
503
504
# File 'lib/kward/config_files.rb', line 502

def skills
  skills_registry.skills
end

.skills_registryObject



544
545
546
547
548
549
550
551
552
# File 'lib/kward/config_files.rb', line 544

def skills_registry
  Skills::Registry.new(
    config_dir: config_dir,
    skill_class: Skill,
    max_file_bytes: MAX_SKILL_FILE_BYTES,
    markdown_parser: method(:markdown_parts),
    inside_directory: method(:inside_directory?)
  )
end

.time_of_day_bucket(now) ⇒ Object



486
487
488
489
490
491
492
493
# File 'lib/kward/config_files.rb', line 486

def time_of_day_bucket(now)
  hour = now.hour
  return "morning" if hour >= 5 && hour < 11
  return "before_lunch" if hour == 11
  return "late_evening" if hour >= 21 || hour < 5

  nil
end

.update_config(values, path = config_path) ⇒ Object

Merges top-level config values and writes the updated config privately.



144
145
146
147
148
149
150
151
# File 'lib/kward/config_files.rb', line 144

def update_config(values, path = config_path)
  raise "Config values must be an object" unless values.is_a?(Hash)

  config = read_config(path)
  values.each { |key, value| config[key.to_s] = value }
  write_config(config, path)
  config
end

.update_overlay_settings(values) ⇒ Object

Validates and persists terminal overlay settings.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/kward/config_files.rb', line 223

def update_overlay_settings(values)
  raise "Overlay settings must be an object" unless values.is_a?(Hash)

  config = read_config
  overlay = config["overlay"].is_a?(Hash) ? config["overlay"].dup : {}
  values.each do |key, value|
    key = key.to_s
    value = value.to_s
    case key
    when "alignment"
      raise "Overlay alignment must be left, center, or right" unless OVERLAY_ALIGNMENTS.include?(value)
    when "width"
      raise "Overlay width must be capped or maximum" unless OVERLAY_WIDTHS.include?(value)
    else
      raise "Unknown overlay setting: #{key}"
    end
    overlay[key] = value
  end
  config["overlay"] = overlay
  write_config(config)
  overlay_settings(config)
end

.web_search_config(config = read_config) ⇒ Object

Returns the nested web-search config object, or an empty config when absent.



217
218
219
220
# File 'lib/kward/config_files.rb', line 217

def web_search_config(config = read_config)
  value = config["web_search"]
  value.is_a?(Hash) ? value : {}
end

.weekday_name(now) ⇒ Object



495
496
497
# File 'lib/kward/config_files.rb', line 495

def weekday_name(now)
  %w[sunday monday tuesday wednesday thursday friday saturday][now.wday]
end

.workspace_agents_file?(workspace_root) ⇒ Boolean

Returns:

  • (Boolean)


372
373
374
# File 'lib/kward/config_files.rb', line 372

def workspace_agents_file?(workspace_root)
  File.exist?(workspace_agents_path(workspace_root))
end

.workspace_agents_path(workspace_root) ⇒ Object



368
369
370
# File 'lib/kward/config_files.rb', line 368

def workspace_agents_path(workspace_root)
  File.join(canonical_workspace_root(workspace_root), "AGENTS.md")
end

.workspace_agents_prompt(workspace_root) ⇒ Object



376
377
378
# File 'lib/kward/config_files.rb', line 376

def workspace_agents_prompt(workspace_root)
  read_prompt_file(workspace_agents_path(workspace_root), "workspace AGENTS.md")
end

.workspace_guardrails_enabled?(config = read_config) ⇒ Boolean

Returns whether file tools must stay inside the active workspace root.

Returns:

  • (Boolean)


199
200
201
202
# File 'lib/kward/config_files.rb', line 199

def workspace_guardrails_enabled?(config = read_config)
  tools = config["tools"].is_a?(Hash) ? config["tools"] : {}
  tools["workspace_guardrails"] != false
end

.write_config(config, path = config_path) ⇒ Object

Writes config JSON using private file permissions.

Parameters:

  • config (Hash)

    config object to persist

  • path (String) (defaults to: config_path)

    config file path



139
140
141
# File 'lib/kward/config_files.rb', line 139

def write_config(config, path = config_path)
  PrivateFile.write_json(path, config)
end