@@ -161,6 +161,8 @@ def _json_candidates(text: str) -> list[tuple[Any, str]]:
161161
162162
163163def _expected_json_shape (tag : str ) -> type | None :
164+ if tag == "env_audit_json" :
165+ return dict
164166 if tag == "findings_json" :
165167 return list
166168 if tag == "triage_json" :
@@ -175,6 +177,8 @@ def _matches_expected_json(value: Any, expected: type | None, tag: str) -> bool:
175177 return all (isinstance (item , dict ) for item in value )
176178 if tag == "triage_json" :
177179 return isinstance (value .get ("findings" ), list )
180+ if tag == "env_audit_json" :
181+ return isinstance (value .get ("required_env" ), list )
178182 return True
179183
180184
@@ -249,6 +253,49 @@ def _threat_prompt(repo_meta: dict[str, Any], stack: dict[str, Any]) -> str:
249253"""
250254
251255
256+ def _env_audit_prompt (repo_meta : dict [str , Any ], stack : dict [str , Any ]) -> str :
257+ return f"""\
258+ Determine the environment variables required to run the repository mounted read-only at { REPO_MOUNT }
259+ as a meaningful local/staging application, not merely to compile a public page.
260+
261+ Repository metadata:
262+ { json .dumps (repo_meta , indent = 2 )}
263+
264+ Detected stack hints:
265+ { json .dumps (stack , indent = 2 )}
266+
267+ Inspect source and configuration only. Do not execute project code. Decide which env vars a user
268+ would need to configure for the app's primary runtime functionality to work in local/staging,
269+ including auth/session, database, first-party integrations, webhooks, billing if it is part of the
270+ product, scheduled/background workflows, and worker/sandbox callbacks that the app itself invokes.
271+ Use package scripts, app framework config, middleware, root layouts, server/client initialization
272+ code, app route handlers, workflow/worker definitions, .env.example, docs, and CI configuration as
273+ evidence.
274+
275+ Exclude variables only when they are clearly unrelated to running the application: tests/fixtures,
276+ lint/build tooling, CI/deploy metadata, analytics or observability that safely degrades, source-map
277+ upload settings, alternate provider fallbacks not used by the primary configured path, or
278+ vulnerability verification personas.
279+
280+ For each required variable, include:
281+ - name
282+ - confidence: high|medium|low
283+ - reason: one sentence explaining why app runtime needs it
284+ - sources: list of repo-relative evidence files
285+
286+ Return only:
287+ <env_audit_json>
288+ {{ "required_env": [ ... ], "optional_env": [ ... ], "summary": {{ ... }} }}
289+ </env_audit_json>
290+ <env_audit_markdown>
291+ ...human-readable markdown...
292+ </env_audit_markdown>
293+
294+ The <env_audit_json> tag is mandatory. If no variables are required to boot, return
295+ {{ "required_env": [], "optional_env": [], "summary": {{ "required": 0 }} }}.
296+ """
297+
298+
252299def _scan_prompt (threat_model : str , max_findings : int , dependency_advisory_summary : str ) -> str :
253300 return f"""\
254301 Perform a static vulnerability scan of the repository mounted read-only at { REPO_MOUNT } .
@@ -354,6 +401,30 @@ def _triage_prompt(
354401
355402def _repair_prompt (tag : str , previous_text : str , context : dict [str , Any ] | None = None ) -> str :
356403 context_text = json .dumps (context or {}, indent = 2 )
404+ if tag == "env_audit_json" :
405+ return f"""\
406+ The previous runtime environment audit did not emit the required machine-readable artifact.
407+ Reformat the previous answer into the exact artifacts below. Use only the previous answer and
408+ context; do not invent required env vars that were not stated.
409+
410+ Context:
411+ <context_json>
412+ { context_text }
413+ </context_json>
414+
415+ Previous answer:
416+ <previous_answer>
417+ { previous_text }
418+ </previous_answer>
419+
420+ Return only:
421+ <env_audit_json>
422+ {{ "required_env": [ ... ], "optional_env": [ ... ], "summary": {{ ... }} }}
423+ </env_audit_json>
424+ <env_audit_markdown>
425+ ...human-readable markdown summary...
426+ </env_audit_markdown>
427+ """
357428 if tag == "findings_json" :
358429 return f"""\
359430 The previous static vulnerability scan did not emit the required machine-readable artifact.
@@ -406,6 +477,92 @@ def _repair_prompt(tag: str, previous_text: str, context: dict[str, Any] | None
406477 raise ValueError (f"unsupported repair tag: { tag } " )
407478
408479
480+ async def run_repo_env_audit (
481+ * ,
482+ repo_path : Path ,
483+ provider : str ,
484+ model : str ,
485+ agent_env : dict [str , str ],
486+ results_dir : Path ,
487+ pi_config_dir : Path | None = None ,
488+ sandbox_mode : str = "docker" ,
489+ out_dir : Path | None = None ,
490+ event_sink : EventSinkLike = NULL_EVENT_SINK ,
491+ ) -> dict [str , Any ]:
492+ repo_path = repo_path .resolve ()
493+ backend = create_execution_backend (sandbox_mode )
494+ out_dir = out_dir or allocate_repo_run_dir (results_dir , repo_path )
495+ out_dir .mkdir (parents = True , exist_ok = True )
496+ repo_meta = {"path" : str (repo_path ), ** _git_metadata (repo_path )}
497+ stack = _detect_stack (repo_path )
498+
499+ audit_text , err = await _run_stage (
500+ prompt = _env_audit_prompt (repo_meta , stack ),
501+ provider = provider ,
502+ model = model ,
503+ agent_env = agent_env ,
504+ repo_path = repo_path ,
505+ pi_config_dir = pi_config_dir ,
506+ container_name = f"repo_{ _safe_repo_name (repo_path )} _env" ,
507+ transcript_path = out_dir / "env_audit_transcript.jsonl" ,
508+ progress_prefix = "[repo:env]" ,
509+ primary_tag = "env_audit_json" ,
510+ allow_web = False ,
511+ backend = backend ,
512+ event_sink = event_sink ,
513+ )
514+ audit_value , _audit_source = _extract_json_artifact (audit_text , "env_audit_json" )
515+ if audit_value is _MISSING or not isinstance (audit_value , dict ):
516+ repair_text , repair_err = await _run_stage (
517+ prompt = _repair_prompt (
518+ "env_audit_json" ,
519+ audit_text ,
520+ {"repo" : repo_meta , "stack" : stack , "transport_error" : err },
521+ ),
522+ provider = provider ,
523+ model = model ,
524+ agent_env = agent_env ,
525+ repo_path = repo_path ,
526+ pi_config_dir = pi_config_dir ,
527+ container_name = f"repo_{ _safe_repo_name (repo_path )} _env_repair" ,
528+ transcript_path = out_dir / "env_audit_repair_transcript.jsonl" ,
529+ progress_prefix = "[repo:env:repair]" ,
530+ primary_tag = "env_audit_json" ,
531+ allow_web = False ,
532+ tools = [],
533+ backend = backend ,
534+ event_sink = event_sink ,
535+ )
536+ repaired_value , _repaired_source = _extract_json_artifact (repair_text , "env_audit_json" )
537+ if repaired_value is _MISSING or not isinstance (repaired_value , dict ):
538+ errors = [item for item in (err , repair_err , "env_audit: missing artifact" ) if item ]
539+ audit_value = {"required_env" : [], "optional_env" : [], "summary" : {}, "errors" : errors }
540+ else :
541+ audit_text = repair_text
542+ audit_value = repaired_value
543+ elif err :
544+ audit_value = {** audit_value , "errors" : [err ]}
545+
546+ audit_markdown = parse_xml_tag (audit_text , "env_audit_markdown" ) or env_audit_markdown (
547+ audit_value
548+ )
549+ _write (
550+ out_dir / "ENV-AUDIT.json" ,
551+ json .dumps (audit_value , indent = 2 ),
552+ event_sink = event_sink ,
553+ stage = "repo:env" ,
554+ artifact_type = "env_audit_json" ,
555+ )
556+ _write (
557+ out_dir / "ENV-AUDIT.md" ,
558+ audit_markdown ,
559+ event_sink = event_sink ,
560+ stage = "repo:env" ,
561+ artifact_type = "env_audit_markdown" ,
562+ )
563+ return audit_value
564+
565+
409566async def _run_stage (
410567 * ,
411568 prompt : str ,
@@ -767,6 +924,27 @@ async def run_repo_static(
767924 )
768925
769926
927+ def env_audit_markdown (audit : dict [str , Any ]) -> str :
928+ lines = ["# Runtime Environment Audit" , "" ]
929+ required = audit .get ("required_env" ) if isinstance (audit .get ("required_env" ), list ) else []
930+ if required :
931+ lines += ["## Required" , "" ]
932+ for item in required :
933+ if isinstance (item , str ):
934+ lines .append (f"- `{ item } `" )
935+ elif isinstance (item , dict ):
936+ name = item .get ("name" ) or item .get ("variable" ) or item .get ("env" )
937+ reason = item .get ("reason" ) or item .get ("evidence" )
938+ suffix = f" - { reason } " if reason else ""
939+ lines .append (f"- `{ name } `{ suffix } " )
940+ else :
941+ lines .append ("No required runtime environment variables were identified." )
942+ if audit .get ("errors" ):
943+ lines += ["" , "## Errors" , "" ]
944+ lines .extend (f"- { error } " for error in audit ["errors" ])
945+ return "\n " .join (lines )
946+
947+
770948def _run_markdown (summary : dict [str , Any ]) -> str :
771949 return f"""\
772950 # Autonomous Repo Static Review
0 commit comments