Skip to content

Commit 7849f3b

Browse files
committed
feature: support JSON config as well as YAML (#1490)
2 parents 64af35f + c62980f commit 7849f3b

File tree

6 files changed

+491
-98
lines changed

6 files changed

+491
-98
lines changed

docs/source/_config.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Config values are loaded in the following priority (lowest-first):
1515

1616
* Plugin defaults in the code
1717
* Core config: from ``garak/resources/garak.core.yaml``; not to be overridden
18-
* Site config: from ``$HOME/.config/garak/garak.site.yaml``
19-
* Runtime config: from an optional config file specified manually, via e.g. CLI parameter
18+
* Site config: from ``$HOME/.config/garak/garak.site.yaml`` or ``garak.site.json``
19+
* Runtime config: from an optional config file (YAML or JSON) specified manually, via e.g. CLI parameter
2020
* Command-line options
2121

2222

docs/source/configurable.rst

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Specifying Custom Configuration
1414
garak can be configured in multiple ways:
1515

1616
* Via command-line parameters
17-
* Using YAML configs
17+
* Using YAML or JSON config files
1818
* Through specifying JSON on the command line
1919

20-
The easiest way is often to use a YAML config, and how to do that is
20+
The easiest way is often to use a config file (YAML or JSON), and how to do that is
2121
described below.
2222

2323
Garak Config Hierarchy
@@ -28,13 +28,13 @@ Configuration values can come from multiple places. At garak load, the
2828
the priority of which values go where. The hierarchy is as follows:
2929

3030
1. Values given at the command line
31-
2. Config values given in a YAML file passed via ``--config``
32-
3. Values in a YAML site config, ``garak.site.yaml``, placed in the config directory (``XDG_CONFIG_DIR``, which is ``~/.config/garak/`` on Linux; see XDG spec for details)
31+
2. Config values given in a YAML or JSON file passed via ``--config``
32+
3. Values in a YAML or JSON site config, ``garak.site.yaml``, ``garak.site.yml``, or ``garak.site.json``, placed in the config directory (``XDG_CONFIG_DIR``, which is ``~/.config/garak/`` on Linux; see XDG spec for details)
3333
4. Fixed values kept in the garak core config - don't edit this. Package updates will overwrite it, and you might break your garak install. It's in ``garak/resources`` if you want to take a look.
3434
5. Default values specified in plugin code
3535

36-
Config YAML
37-
^^^^^^^^^^^
36+
Config Files (YAML and JSON)
37+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3838

3939
Let's take a look at the core config.
4040

@@ -169,7 +169,12 @@ Bundled Quick Configs
169169
^^^^^^^^^^^^^^^^^^^^^
170170

171171
Garak comes bundled with some quick configs that can be loaded directly using ``--config``.
172-
These don't need the ``.yml`` extension when being requested. They include:
172+
173+
**Note on extensions:** JSON configs can be loaded without the ``.json`` extension (e.g., ``--config fast``).
174+
YAML configs require the explicit ``.yaml`` or ``.yml`` extension (e.g., ``--config fast.yaml`` or ``--config fast.yml``).
175+
Extensions are case-insensitive, so ``.JSON``, ``.YAML``, and ``.YML`` are also accepted.
176+
177+
Bundled configs include:
173178

174179
* ``broad`` - Run all active probes, just once each, for a rapid broad test
175180
* ``fast`` - Go through a selection of light probes; skip extended detectors
@@ -178,17 +183,19 @@ These don't need the ``.yml`` extension when being requested. They include:
178183
* ``notox`` - Scan without any toxicity-inducing probes
179184
* ``tox_and_buffs`` - Go through toxicity & slur probes, using only relevant payloads, and a fast paraphraser
180185

181-
These are great places to look at to get an idea of how garak YAML configs can look.
186+
These are great places to look at to get an idea of how garak configs can look.
182187
Quick configs are stored under ``garak/configs/`` in the source code/install.
183188

184189

185190
Using a Custom Config
186191
^^^^^^^^^^^^^^^^^^^^^
187192

188-
To override values in this we can create a new YAML file and point to it from the
193+
To override values in this we can create a new config file (YAML or JSON) and point to it from the
189194
command line using ``--config``. For example, to select just ``latentinjection``
190195
probes and run each prompt just once:
191196

197+
**YAML format:**
198+
192199
.. code-block:: yaml
193200
194201
---
@@ -199,6 +206,23 @@ probes and run each prompt just once:
199206
probe_spec: latentinjection
200207
201208
If we save this as ``latent1.yaml`` somewhere, then we can use it with ``garak --config latent1.yaml``.
209+
Note: YAML configs require the explicit ``.yaml`` or ``.yml`` extension (case-insensitive).
210+
211+
**JSON format:**
212+
213+
.. code-block:: json
214+
215+
{
216+
"run": {
217+
"generations": 1
218+
},
219+
"plugins": {
220+
"probe_spec": "latentinjection"
221+
}
222+
}
223+
224+
If we save this as ``latent1.json`` somewhere, then we can use it with ``garak --config latent1.json``
225+
or without the extension: ``garak --config latent1``.
202226

203227
Using a Custom JSON Config
204228
^^^^^^^^^^^^^^^^^^^^^^^^^^

garak/_config.py

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections import defaultdict
1212
from dataclasses import dataclass
1313
import importlib
14+
import json
1415
import logging
1516
import os
1617
import stat
@@ -155,13 +156,22 @@ def _combine_into(d: dict, combined: dict) -> dict:
155156
return combined
156157

157158

158-
def _load_yaml_config(settings_filenames) -> dict:
159+
def _load_config_files(settings_filenames) -> dict:
159160
global config_files
160161
config_files += settings_filenames
161162
config = nested_dict()
162163
for settings_filename in settings_filenames:
163164
with open(settings_filename, encoding="utf-8") as settings_file:
164-
settings = yaml.safe_load(settings_file)
165+
try:
166+
settings = json.load(settings_file)
167+
except json.JSONDecodeError:
168+
settings_file.seek(0)
169+
try:
170+
settings = yaml.safe_load(settings_file)
171+
except yaml.YAMLError as e:
172+
message = f"Failed to parse config file {settings_filename} as JSON or YAML: {e}"
173+
logging.error(message)
174+
raise ValueError(message) from e
165175
if settings is not None:
166176
if _key_exists(settings, "api_key"):
167177
if platform.system() == "Windows":
@@ -216,7 +226,7 @@ def _load_yaml_config(settings_filenames) -> dict:
216226

217227
def _store_config(settings_files) -> None:
218228
global system, run, plugins, reporting, version
219-
settings = _load_yaml_config(settings_files)
229+
settings = _load_config_files(settings_files)
220230
system = _set_settings(system, settings["system"])
221231
run = _set_settings(run, settings["run"])
222232
run.user_agent = run.user_agent.replace("{version}", version)
@@ -292,27 +302,99 @@ def load_config(
292302

293303
settings_files = [str(transient.package_dir / "resources" / "garak.core.yaml")]
294304

295-
fq_site_config_filename = str(transient.config_dir / site_config_filename)
296-
if os.path.isfile(fq_site_config_filename):
297-
settings_files.append(fq_site_config_filename)
305+
if site_config_filename == "garak.site.yaml":
306+
site_config_json = str(transient.config_dir / "garak.site.json")
307+
site_config_yaml = str(transient.config_dir / "garak.site.yaml")
308+
site_config_yml = str(transient.config_dir / "garak.site.yml")
309+
310+
has_json = os.path.isfile(site_config_json)
311+
has_yaml = os.path.isfile(site_config_yaml)
312+
has_yml = os.path.isfile(site_config_yml)
313+
314+
if sum([has_json, has_yaml, has_yml]) > 1:
315+
message = "Multiple site config files found (garak.site.json, garak.site.yaml, garak.site.yml). Please use only one site config format."
316+
logging.error(message)
317+
raise ValueError(message)
318+
elif has_json:
319+
settings_files.append(site_config_json)
320+
elif has_yaml:
321+
settings_files.append(site_config_yaml)
322+
elif has_yml:
323+
settings_files.append(site_config_yml)
324+
else:
325+
logging.debug(
326+
"no site config found at: %s, %s, or %s",
327+
site_config_json,
328+
site_config_yaml,
329+
site_config_yml,
330+
)
298331
else:
299-
# warning, not error, because this one has a default value
300-
logging.debug("no site config found at: %s", fq_site_config_filename)
332+
fq_site_config_filename = str(transient.config_dir / site_config_filename)
333+
if os.path.isfile(fq_site_config_filename):
334+
settings_files.append(fq_site_config_filename)
335+
else:
336+
logging.debug("no site config found at: %s", fq_site_config_filename)
301337

302338
if run_config_filename is not None:
303-
# take config file path as provided
339+
# If file exists as-is, use it
304340
if os.path.isfile(run_config_filename):
305341
settings_files.append(run_config_filename)
306-
elif os.path.isfile(
307-
str(transient.package_dir / "configs" / (run_config_filename + ".yaml"))
308-
):
309-
settings_files.append(
310-
str(transient.package_dir / "configs" / (run_config_filename + ".yaml"))
311-
)
342+
# If explicit extension, check bundled
343+
elif run_config_filename.lower().endswith((".json", ".yaml", ".yml")):
344+
bundled = str(transient.package_dir / "configs" / run_config_filename)
345+
if os.path.isfile(bundled):
346+
settings_files.append(bundled)
347+
else:
348+
message = f"run config not found: {run_config_filename}"
349+
logging.error(message)
350+
raise FileNotFoundError(message)
351+
# Extension-less: JSON-only, YAML needs explicit .yaml/.yml
312352
else:
313-
message = f"run config not found: {run_config_filename}"
314-
logging.error(message)
315-
raise FileNotFoundError(message)
353+
json_path = run_config_filename + ".json"
354+
yaml_path = run_config_filename + ".yaml"
355+
yml_path = run_config_filename + ".yml"
356+
json_bundled = str(
357+
transient.package_dir / "configs" / (run_config_filename + ".json")
358+
)
359+
yaml_bundled = str(
360+
transient.package_dir / "configs" / (run_config_filename + ".yaml")
361+
)
362+
yml_bundled = str(
363+
transient.package_dir / "configs" / (run_config_filename + ".yml")
364+
)
365+
366+
has_json = os.path.isfile(json_path)
367+
has_yaml = os.path.isfile(yaml_path)
368+
has_yml = os.path.isfile(yml_path)
369+
has_json_bundled = os.path.isfile(json_bundled)
370+
has_yaml_bundled = os.path.isfile(yaml_bundled)
371+
has_yml_bundled = os.path.isfile(yml_bundled)
372+
373+
# Direct path: JSON-only, warn if both exist
374+
if has_json or has_yaml or has_yml:
375+
if has_json and (has_yaml or has_yml):
376+
yaml_ext = ".yaml" if has_yaml else ".yml"
377+
logging.warning(
378+
f"Both {run_config_filename}.json and {yaml_ext} found. Using .json"
379+
)
380+
if has_json:
381+
settings_files.append(json_path)
382+
else:
383+
yaml_ext = ".yaml" if has_yaml else ".yml"
384+
message = f"Found {run_config_filename}{yaml_ext} but YAML needs explicit .yaml/.yml extension"
385+
logging.error(message)
386+
raise FileNotFoundError(message)
387+
elif has_json_bundled:
388+
settings_files.append(json_bundled)
389+
elif has_yaml_bundled or has_yml_bundled:
390+
yaml_ext = ".yaml" if has_yaml_bundled else ".yml"
391+
message = f"Found {run_config_filename}{yaml_ext} but YAML needs explicit .yaml/.yml extension"
392+
logging.error(message)
393+
raise FileNotFoundError(message)
394+
else:
395+
message = f"run config not found: {run_config_filename}"
396+
logging.error(message)
397+
raise FileNotFoundError(message)
316398

317399
logging.debug("Loading configs from: %s", ",".join(settings_files))
318400
_store_config(settings_files=settings_files)

garak/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def main(arguments=None) -> None:
135135
help="number of generations per prompt",
136136
)
137137
parser.add_argument(
138-
"--config", type=str, default=None, help="YAML config file for this run"
138+
"--config", type=str, default=None, help="YAML or JSON config file for this run"
139139
)
140140

141141
## PLUGINS
@@ -298,7 +298,12 @@ def main(arguments=None) -> None:
298298
# load site config before loading CLI config
299299
_cli_config_supplied = args.config is not None
300300
prior_user_agents = _config.get_http_lib_agents()
301-
_config.load_config(run_config_filename=args.config)
301+
try:
302+
_config.load_config(run_config_filename=args.config)
303+
except FileNotFoundError as e:
304+
logging.exception(e)
305+
print(f"❌{e}")
306+
exit(1)
302307

303308
# extract what was actually passed on CLI; use a masking argparser
304309
aux_parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)

tests/buffs/test_buff_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
def test_include_original_prompt():
2828
# https://github.com/python/cpython/pull/97015 to ensure Windows compatibility
29-
with tempfile.NamedTemporaryFile(buffering=0, delete=False) as tmp:
29+
with tempfile.NamedTemporaryFile(buffering=0, delete=False, suffix=".yaml") as tmp:
3030
tmp.write(
3131
"""---
3232
plugins:
@@ -66,7 +66,7 @@ def test_include_original_prompt():
6666

6767

6868
def test_exclude_original_prompt():
69-
with tempfile.NamedTemporaryFile(buffering=0, delete=False) as tmp:
69+
with tempfile.NamedTemporaryFile(buffering=0, delete=False, suffix=".yaml") as tmp:
7070
tmp.write(
7171
"""---
7272
plugins:

0 commit comments

Comments
 (0)