@@ -254,6 +254,8 @@ async def test_facade_persists_artifacts_under_output_dir(tmp_path, monkeypatch)
254254 """The facade must materialise result.json, summary.txt, rounds/*.json,
255255 baseline_prompts/, best_prompts/, config.snapshot.json and run.log under
256256 output_dir for every successful run."""
257+ import json
258+
257259 train = EvalSet (eval_set_id = "train" , eval_cases = [_eval_case ("c1" )])
258260 val = EvalSet (eval_set_id = "val" , eval_cases = [_eval_case ("c1" )])
259261 train_path = tmp_path / "train.json"
@@ -287,7 +289,14 @@ async def fake_call_gepa(self, **kwargs):
287289
288290 assert (output_dir / "result.json" ).is_file ()
289291 assert (output_dir / "summary.txt" ).is_file ()
290- assert (output_dir / "config.snapshot.json" ).is_file ()
292+ config_snapshot_path = output_dir / "config.snapshot.json"
293+ assert config_snapshot_path .is_file ()
294+ config_snapshot_text = config_snapshot_path .read_text (encoding = "utf-8" )
295+ assert "test-key" not in config_snapshot_text
296+ config_snapshot = json .loads (config_snapshot_text )
297+ assert config_snapshot ["optimize" ]["algorithm" ]["reflection_lm" ][
298+ "api_key"
299+ ] == "<redacted>"
291300 assert (output_dir / "run.log" ).is_file ()
292301 assert (output_dir / "baseline_prompts" / "instruction.md" ).is_file ()
293302 assert (output_dir / "best_prompts" / "instruction.md" ).is_file ()
@@ -297,6 +306,114 @@ async def fake_call_gepa(self, **kwargs):
297306 assert "SUCCEEDED" in log_line
298307
299308
309+ def test_copy_config_snapshot_recursively_redacts_common_secret_keys (tmp_path ):
310+ """Config snapshots must remain useful without publishing credentials."""
311+ import json
312+
313+ payload = {
314+ "api_key" : "api-secret" ,
315+ "nested" : [
316+ {
317+ "TOKEN" : "token-secret" ,
318+ "access-token" : "access-secret" ,
319+ "Authorization" : "Bearer auth-secret" ,
320+ },
321+ {
322+ "password" : "password-secret" ,
323+ "credentials" : {"username" : "alice" , "password" : "nested-secret" },
324+ "privateKey" : "private-secret" ,
325+ },
326+ {
327+ "openai_api_key" : "namespaced-api-secret" ,
328+ "github_token" : "namespaced-token-secret" ,
329+ "aws_secret_access_key" : "aws-secret" ,
330+ "db_passwd" : "database-secret" ,
331+ },
332+ ],
333+ "model_name" : "gpt-4o" ,
334+ "max_tokens" : 128 ,
335+ "token_budget" : 256 ,
336+ }
337+ config_path = tmp_path / "optimizer.json"
338+ config_path .write_text (json .dumps (payload ), encoding = "utf-8" )
339+ output_dir = tmp_path / "output"
340+ output_dir .mkdir ()
341+
342+ AgentOptimizer ._copy_config_snapshot (str (config_path ), str (output_dir ))
343+
344+ snapshot_path = output_dir / "config.snapshot.json"
345+ snapshot_text = snapshot_path .read_text (encoding = "utf-8" )
346+ snapshot = json .loads (snapshot_text )
347+ assert snapshot == {
348+ "api_key" : "<redacted>" ,
349+ "nested" : [
350+ {
351+ "TOKEN" : "<redacted>" ,
352+ "access-token" : "<redacted>" ,
353+ "Authorization" : "<redacted>" ,
354+ },
355+ {
356+ "password" : "<redacted>" ,
357+ "credentials" : "<redacted>" ,
358+ "privateKey" : "<redacted>" ,
359+ },
360+ {
361+ "openai_api_key" : "<redacted>" ,
362+ "github_token" : "<redacted>" ,
363+ "aws_secret_access_key" : "<redacted>" ,
364+ "db_passwd" : "<redacted>" ,
365+ },
366+ ],
367+ "model_name" : "gpt-4o" ,
368+ "max_tokens" : 128 ,
369+ "token_budget" : 256 ,
370+ }
371+ assert snapshot_text == (
372+ json .dumps (
373+ snapshot ,
374+ ensure_ascii = False ,
375+ sort_keys = True ,
376+ indent = 2 ,
377+ allow_nan = False ,
378+ )
379+ + "\n "
380+ )
381+ for secret in (
382+ "api-secret" ,
383+ "token-secret" ,
384+ "access-secret" ,
385+ "auth-secret" ,
386+ "password-secret" ,
387+ "nested-secret" ,
388+ "private-secret" ,
389+ "namespaced-api-secret" ,
390+ "namespaced-token-secret" ,
391+ "aws-secret" ,
392+ "database-secret" ,
393+ ):
394+ assert secret not in snapshot_text
395+
396+
397+ @pytest .mark .parametrize (
398+ "invalid_config" ,
399+ [
400+ '{"api_key": "secret", invalid}' ,
401+ '{"api_key": NaN}' ,
402+ ],
403+ )
404+ def test_copy_config_snapshot_invalid_json_fails_closed (tmp_path , invalid_config ):
405+ """Malformed source config must never be copied verbatim as an artifact."""
406+ config_path = tmp_path / "optimizer.json"
407+ config_path .write_text (invalid_config , encoding = "utf-8" )
408+ output_dir = tmp_path / "output"
409+ output_dir .mkdir ()
410+
411+ AgentOptimizer ._copy_config_snapshot (str (config_path ), str (output_dir ))
412+
413+ assert not (output_dir / "config.snapshot.json" ).exists ()
414+ assert not (output_dir / "config.snapshot.json.tmp" ).exists ()
415+
416+
300417@pytest .mark .asyncio
301418async def test_facade_persists_artifacts_when_algorithm_fails (tmp_path , monkeypatch ):
302419 """Even when the algorithm returns a FAILED result the facade should
0 commit comments