Current section
Files
Jump to
Current section
Files
lib/codex/config/layer_stack.ex
defmodule Codex.Config.LayerStack do
@moduledoc false
@type layer_source :: :system | :user | :project
@type layer :: %{
source: layer_source(),
path: String.t(),
config: map()
}
@default_system_config_path "/etc/codex/config.toml"
@default_project_root_markers [".git"]
@spec load(String.t(), String.t() | nil) :: {:ok, [layer()]} | {:error, term()}
def load(codex_home, cwd \\ nil) when is_binary(codex_home) do
cwd = normalize_cwd(cwd)
with {:ok, base_layers} <- load_base_layers(codex_home) do
case cwd do
nil ->
{:ok, base_layers}
_ ->
with {:ok, project_layers} <- load_project_layers(base_layers, cwd) do
{:ok, base_layers ++ project_layers}
end
end
end
end
@spec effective_config([layer()]) :: map()
def effective_config(layers) when is_list(layers) do
Enum.reduce(layers, %{}, fn layer, acc -> merge_configs(acc, layer.config) end)
end
@spec remote_models_enabled?(String.t(), String.t() | nil) :: boolean()
def remote_models_enabled?(codex_home, cwd \\ nil) when is_binary(codex_home) do
case load(codex_home, cwd) do
{:ok, layers} ->
layers
|> effective_config()
|> get_in(["features", "remote_models"])
|> Kernel.===(true)
{:error, _reason} ->
false
end
end
defp load_base_layers(codex_home) do
with {:ok, system_layers} <- load_system_layers() do
user_path = Path.join(codex_home, "config.toml")
case read_required_config(user_path) do
{:ok, config} ->
{:ok, system_layers ++ [%{source: :user, path: user_path, config: config}]}
{:error, _} = error ->
error
end
end
end
defp load_system_layers do
case read_optional_config(system_config_path()) do
{:ok, config} -> {:ok, [%{source: :system, path: system_config_path(), config: config}]}
:missing -> {:ok, []}
{:error, _} = error -> error
end
end
defp load_project_layers(base_layers, cwd) do
base_config = effective_config(base_layers)
with {:ok, markers} <- project_root_markers(base_config) do
project_root = find_project_root(cwd, markers || @default_project_root_markers)
load_project_layers_between(project_root, cwd)
end
end
defp load_project_layers_between(project_root, cwd) do
dirs = dirs_between(project_root, cwd)
Enum.reduce_while(dirs, {:ok, []}, fn dir, {:ok, layers} ->
case project_layer_for_dir(dir) do
{:ok, nil} ->
{:cont, {:ok, layers}}
{:ok, layer} ->
{:cont, {:ok, layers ++ [layer]}}
{:error, _} = error ->
{:halt, error}
end
end)
end
defp project_layer_for_dir(dir) do
dot_codex = Path.join(dir, ".codex")
if File.dir?(dot_codex) do
config_path = Path.join(dot_codex, "config.toml")
case read_project_config(config_path) do
{:ok, config} -> {:ok, %{source: :project, path: config_path, config: config}}
{:error, _} = error -> error
end
else
{:ok, nil}
end
end
defp project_root_markers(config) when is_map(config) do
case Map.get(config, "project_root_markers") do
nil ->
{:ok, nil}
markers when is_list(markers) ->
if Enum.all?(markers, &is_binary/1) do
{:ok, markers}
else
{:error, {:invalid_project_root_markers, markers}}
end
other ->
{:error, {:invalid_project_root_markers, other}}
end
end
defp read_optional_config(path) do
case File.read(path) do
{:ok, contents} -> parse_config(contents, path)
{:error, :enoent} -> :missing
{:error, reason} -> {:error, {:config_read_failed, path, reason}}
end
end
defp read_required_config(path) do
case File.read(path) do
{:ok, contents} -> parse_config(contents, path)
{:error, :enoent} -> {:ok, %{}}
{:error, reason} -> {:error, {:config_read_failed, path, reason}}
end
end
defp read_project_config(path) do
case File.read(path) do
{:ok, contents} -> parse_config(contents, path)
{:error, :enoent} -> {:ok, %{}}
{:error, reason} -> {:error, {:config_read_failed, path, reason}}
end
end
defp parse_config(contents, path) do
case parse_config_contents(contents) do
{:ok, config} -> {:ok, config}
{:error, reason} -> {:error, {:invalid_toml, path, reason}}
end
end
defp parse_config_contents(contents) do
lines = String.split(contents, ~r/\R/)
initial = %{
section: nil,
remote_models: nil,
project_root_markers: nil,
markers_buffer: nil
}
with {:ok, state} <- parse_config_lines(lines, initial) do
build_config(state)
end
end
defp parse_config_lines(lines, state) do
Enum.reduce_while(lines, {:ok, state}, fn line, {:ok, acc} ->
case parse_config_line(line, acc) do
{:ok, next} -> {:cont, {:ok, next}}
{:error, _} = error -> {:halt, error}
end
end)
end
defp parse_config_line(line, %{markers_buffer: buffer} = state) when is_list(buffer) do
stripped = strip_comment(line)
buffer = buffer ++ [stripped]
if String.contains?(stripped, "]") do
raw = Enum.join(buffer, "")
case parse_marker_list(raw) do
{:ok, markers} ->
{:ok, %{state | project_root_markers: markers, markers_buffer: nil}}
{:error, _} = error ->
error
end
else
{:ok, %{state | markers_buffer: buffer}}
end
end
defp parse_config_line(line, state) do
stripped = strip_comment(line)
cond do
stripped == "" ->
{:ok, state}
section_header?(stripped) ->
{:ok, %{state | section: parse_section_header(stripped)}}
state.section in [nil, ""] and key_matches?(stripped, "project_root_markers") ->
parse_project_root_markers(stripped, state)
state.section == "features" and key_matches?(stripped, "remote_models") ->
parse_remote_models(stripped, state)
true ->
{:ok, state}
end
end
defp parse_project_root_markers(line, state) do
case split_kv(line) do
{:ok, _key, value} ->
case parse_marker_list(value) do
{:ok, markers} -> {:ok, %{state | project_root_markers: markers}}
{:error, :incomplete} -> {:ok, %{state | markers_buffer: [value]}}
{:error, _} = error -> error
end
{:error, _} = error ->
error
end
end
defp parse_remote_models(line, state) do
case split_kv(line) do
{:ok, _key, value} ->
case parse_bool(value) do
bool when is_boolean(bool) -> {:ok, %{state | remote_models: bool}}
_ -> {:error, {:invalid_remote_models, value}}
end
{:error, _} = error ->
error
end
end
defp build_config(%{remote_models: remote_models, project_root_markers: markers}) do
config =
%{}
|> maybe_put("features", remote_models)
|> maybe_put("project_root_markers", markers)
{:ok, config}
end
defp maybe_put(config, _key, nil), do: config
defp maybe_put(config, "features", value) when is_boolean(value) do
Map.put(config, "features", %{"remote_models" => value})
end
defp maybe_put(config, "project_root_markers", value) when is_list(value) do
Map.put(config, "project_root_markers", value)
end
defp maybe_put(config, _key, _value), do: config
defp parse_marker_list(value) do
trimmed = String.trim(value)
cond do
!String.contains?(trimmed, "[") ->
{:error, {:invalid_project_root_markers, value}}
String.contains?(trimmed, "]") ->
do_parse_marker_list(trimmed)
true ->
{:error, :incomplete}
end
end
defp do_parse_marker_list(value) do
case Regex.run(~r/\[(.*)\]/s, value) do
[_, inner] ->
inner
|> String.trim()
|> markers_from_inner(value)
_ ->
{:error, {:invalid_project_root_markers, value}}
end
end
defp markers_from_inner("", _value), do: {:ok, []}
defp markers_from_inner(inner, value) do
case extract_markers(inner) do
[] -> {:error, {:invalid_project_root_markers, value}}
markers -> {:ok, markers}
end
end
defp extract_markers(inner) do
Regex.scan(~r/"([^"]*)"/, inner)
|> Enum.map(fn [_full, match] -> match end)
end
defp split_kv(line) do
case String.split(line, "=", parts: 2) do
[key, value] ->
{:ok, String.trim(key), String.trim(value)}
_ ->
{:error, {:invalid_config_line, line}}
end
end
defp key_matches?(line, key) do
case split_kv(line) do
{:ok, ^key, _value} -> true
_ -> false
end
end
defp section_header?(line) do
String.starts_with?(line, "[") && String.ends_with?(line, "]")
end
defp parse_section_header(line) do
line
|> String.trim_leading("[")
|> String.trim_trailing("]")
|> String.trim()
end
defp strip_comment(line) do
line
|> String.split("#", parts: 2)
|> List.first()
|> String.split(";", parts: 2)
|> List.first()
|> String.trim()
end
defp parse_bool(value) do
value
|> String.trim()
|> String.downcase()
|> case do
"true" -> true
"false" -> false
_ -> nil
end
end
defp merge_configs(base, override) when is_map(base) and is_map(override) do
Map.merge(base, override, fn _key, left, right ->
if is_map(left) and is_map(right) do
merge_configs(left, right)
else
right
end
end)
end
defp dirs_between(project_root, cwd) do
project_root = Path.expand(project_root)
cwd = Path.expand(cwd)
dirs =
cwd
|> ancestors()
|> Enum.reduce_while([], fn dir, acc ->
acc = [dir | acc]
if dir == project_root do
{:halt, acc}
else
{:cont, acc}
end
end)
dirs
|> Enum.reverse()
|> case do
[] -> [cwd]
list -> list
end
end
defp ancestors(path) do
path = Path.expand(path)
Stream.unfold(path, fn
nil ->
nil
current ->
parent = Path.dirname(current)
next = if parent == current, do: nil, else: parent
{current, next}
end)
end
defp find_project_root(cwd, []), do: Path.expand(cwd)
defp find_project_root(cwd, markers) when is_list(markers) do
Enum.find(ancestors(cwd), Path.expand(cwd), &has_marker?(&1, markers))
end
defp has_marker?(dir, markers) do
Enum.any?(markers, fn marker ->
File.exists?(Path.join(dir, marker))
end)
end
defp normalize_cwd(nil) do
case File.cwd() do
{:ok, cwd} -> cwd
_ -> nil
end
end
defp normalize_cwd(cwd) when is_binary(cwd), do: cwd
defp normalize_cwd(_), do: nil
defp system_config_path do
case Application.get_env(:codex_sdk, :system_config_path) do
nil -> @default_system_config_path
value -> value
end
end
end