Skip to content

the tool will use file-based configuration

Context

The command line tool review-assist (ra)'s current main purpose is to provide an automated review of git change requests on git servers. The automation relies on the LLM inference server the users provide and connect to the tool (see 0001-byo-llm.md ADR). Git servers can be of different kinds (see 0002-git-server-integration.md ADR). The tool doesn't provide a generic code review but has several review lenses which are all invoked by default.

All these aspects are managed by the user using a configuration system.

Decision

The tool will use the section-based INI format (with .cfg extension) as the source of configuration data. The schema of that configuration file is represented using JSON Schema. The default location is the standard location in UNIX/Linux environments (XDG_CONFIG_HOME). Values for some configuration keys can be overridden by environment variables, CLI parameters or interactive user input.

Example config file, ~/.config/ra/review-assist.cfg:

[meta]
version: 1

[adapters]
local_llm.provider = openai
local_llm.api_url = http://localhost:1234/v1
local_llm.api_key = nokey
local_llm.model = lmstudio-community/gemma-4-e4b-it-mlx
git_server.base_url = https://gitlab.com/api/v4/projects
git_server.private_token = my-token

Example of JSON schema:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "properties": {
        "meta": {
            "properties": {
                "version": {
                    "type": "integer",
                    "const": 1
                }
            }
        },
        "adapters": {
            "properties": {
                "local_llm.provider": {"type": "string"},
                "local_llm.model": {"type": "string"},
                "local_llm.api_url": {"type": "string", "format": "uri"},
                "local_llm.api_key": {"type": "string"},
                "git_server.base_url": {"type": "string", "format": "uri"},
                "git_server.private_token": {"type": "string"}
            },
            "required": ["local_llm.provider", "git_server.base_url"],
            "additionalProperties": false
        },
        "settings": {
            "properties": {
                "review.publish_to_server": {"type": "boolean"},
                "review.ouput_to_terminal": {"type": "boolean"}
            },
            "required": ["publish"],
            "additionalProperties": false
        },
        "review_lenses": {
            "properties": {
                "change_description.enabled": {"type": "boolean"},
                "change_description.custom_skill_path": {"type": "string"},
                "potential_issues.enabled": {"type": "boolean"},
                "potential_issues.custom_skill_path": {"type": "string"},
                "design_recommendations.enabled": {"type": "boolean"},
                "design_recommendations.custom_skill_path": {"type": "string"}
            },
            "required": [],
            "additionalProperties": false
        },
        "preferences": {
            "properties": {
                "coloured_terminal_output": {"type": "boolean"}
            },
            "required": ["coloured_terminal_output"],
            "additionalProperties": false
        }
    },
    "required": ["meta","adapters"],
    "additionalProperties": false
}

To bridge the Dict based configuration and the JSON Schema, we use jsonschema from the standard library (so no additional dependencies), which can take Dict as input and validate it against a JSON Schema.

In addition, I propose that the value for git_server.private_token can be specified from environment variables; and if set, the values for the properties from the "review_lenses" section can be overridden from the command line.

The version key from the meta section is used to track the version of the configuration schema a given configuration file conforms to.

There are going to be contexts in which the tool will write to the configuration file (e.g.: onboarding).

Alternatives considered

The key criteria for me was simplicity for humans to read, edit and reason about the configuration, the ability to validate the configuration syntax (in-band and out-of-band) and minimal dependencies.

Alternatives considered for file format

JSON

Advantages Disadvantages
+ Machine friendly - Not simple to read and edit for human
+ Unlimited nesting
+ No dependencies for reading
+ No dependencies for writing
+ Easy to validate with JSON Schema standard
+ Simple serialisation/deserialisation

XML

Advantages Disadvantages
+ Has sophisticated grammar - Not simple tor read and edit for human
+ Machine friendly - Machinery for serialisation/deserialiseation/validation is complex
- Schema-based Validaton requires installing third party dependencies

TOML

Advantages Disadvantages
+ Simple to read and edit for human - Requires third party library for writing
+ No dependencies for reading (use builtin tomllib) - Can become messy to human for long, complex configuration
+ Simple data structure (dict) when deserialized - Not as well structured as XML or YAML
+ Support multiple levels of nesting - No schema validation
+ Support for arrays

YAML

Advantages Disadvantages
+ Simple to read and edit for human - Requires third party library for reading
+ Has sophisticated grammar - Requires third party library for writing
+ Machine friendly - No schema validation
+ Support multiple levels of nesting
+ Support for arrays

CLI parameters only

An alternative to a configuration file is to use tool parameters for all the settings. Although it allows for a quick way to pass different settings over multiple runs, I see several problems with that approach: * The UX will get bad as the number of options to pass every time increases * The arguments parsing logic will become complex * Terminal history is fragile for persistence of user preferences

It can still be combined with the configuration file for options that are likely to be adjusted for each invocation. In this tool, that's the project id and the change request ID.

Environment variables only

That approach can be tedious for the user to change and access if there are many options. It is also a bad UX if set for many options as a prefix to the tool evocation in the terminal. Set environment variables are ephemeral as they are tied to the current session. The user would have to show extra effort to persist them.

For options that almost never change and/or whose values depend only on which environment the tool is run, and/or whose values are sensitive, environment variables are a good medium.

That's why, while relying on a configuration file, the tool allows git server tokens to be specified from environment variables.

Alternatives considered for schema validation

  • Pydantic
  • Marshmallow

For validation, I prefer having an explicit schema I validate an object against, than having the validator's semantics intertwine with the type system and class hierarchy like Pydantic does; that's why for custom validation,
I normally prefer Marshmallow. But because I expect the configuration to be handled and validated in context outside python environments (e.g. CI/CD jobs), I prefer for this tool to rely on an open, language agnostic standard. JSON Schema appears to be the ideal choice for that

Consequences

Gains

  • No additional dependencies
  • Simplicity for user to read and edit the configuration
  • Can be validated with JSON Schema

Need to be managed

  • Only one level of nesting
  • No sophisticated grammar to structure the configuration
  • Handling sensitive data (the git server private token)

The first issue is alleviated by using materialised paths when naming the keys in the configuration file (e.g.: local_llm.provider).

The second issue is alleviated by: * Ensuring we have a versioned schema to validate the configuration. * Leveraging configparser's one-level nesting and our application of materialised paths. * Ensuring strong typing for unserialised configuration data to the same standard as the rest of the codebase. * Sharing the responsibility of configuring the tool between: * configuration file, * environment variables, * and command line parameters * interactive user input.

The last point is done by assigning which options come from which sources, and in the case we allow an option to come from multiple sources, we set up a precedence order policy.

Options Sources Hard-coded values Configuration file Environment variables Command line parameters Interactive user input
local_llm.* yes (2) yes (1) no no no
git_server.base_url no yes no no no
git_server.private_token no yes (1) yes (2) no yes (3)
review.* no yes no no no
change_description.* yes (1) yes (2) no no no
potential_issues.* yes (1) yes (2) no no no
design_recommendations.* yes (1) yes (2) no no no
project id/namespace no no no yes (1) yes (2)
change request id no no no yes (1) yes (2)

The number in parentheses denotes which source should be queried first for the options that can be set from multiple sources.

On a side note, configuration loading will be abstracted behind a ConfigRepository class to isolate parsing logic, but this will be covered with more details in the next ADR on hexagonal architecture, so there won't be any details here.

Finally, to handle the git server private token, we should: * Ensure the configuration file permissions are set to 0600. * Ensure it is redacted when attempting to print/log its value (use a masking value wrapper)

Accepted drawbacks

  • Not machine friendly