Skip to content

Commit b26be8c

Browse files
authored
Merge pull request #8 from superagent-ai/homanp/early-runtime-preflight
feat: Run live runtime preflight before static scan
2 parents a063c68 + 27f46e9 commit b26be8c

5 files changed

Lines changed: 275 additions & 52 deletions

File tree

pithos/cli.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
from .events import event_mode_from_env, make_event_sink
1818
from .exec_backend import SANDBOX_MODES
19-
from .repo_runner import DEFAULT_REPO_VOTES, run_repo_static
19+
from .repo_runner import DEFAULT_REPO_VOTES, allocate_repo_run_dir, run_repo_static
2020
from .repo_source import DEFAULT_REPO_CACHE, redact_secrets, resolve_repo_source
2121
from .runtime_template import generate_runtime_profile_template, write_runtime_profile_template
22-
from .runtime_verifier import run_verify_repo
22+
from .runtime_verifier import run_runtime_preflight, run_verify_repo
2323
from . import sandbox
2424

2525

@@ -331,6 +331,24 @@ def _cmd_run(args: argparse.Namespace) -> int:
331331
print(file=out)
332332

333333
try:
334+
run_out_dir = (
335+
allocate_repo_run_dir(args.results_dir, repo_path) if args.execute_app else None
336+
)
337+
runtime_preflight = None
338+
if args.execute_app:
339+
runtime_preflight = run_runtime_preflight(
340+
repo_path=repo_path,
341+
profile_path=args.runtime_profile,
342+
results_dir=run_out_dir / "verify" if run_out_dir else None,
343+
execute_app=args.execute_app,
344+
allow_inferred_runtime=args.allow_inferred_runtime,
345+
provider=args.provider,
346+
model=args.model,
347+
agent_env=agent_env,
348+
pi_config_dir=pi_config_dir,
349+
sandbox_mode=args.sandbox_mode,
350+
event_sink=event_sink,
351+
)
334352
scan = asyncio.run(
335353
run_repo_static(
336354
repo_path=repo_path,
@@ -344,6 +362,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
344362
allow_web=allow_web,
345363
collect_advisories=args.advisories,
346364
sandbox_mode=args.sandbox_mode,
365+
out_dir=run_out_dir,
347366
event_sink=event_sink,
348367
)
349368
)
@@ -360,6 +379,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
360379
agent_env=agent_env,
361380
pi_config_dir=pi_config_dir,
362381
sandbox_mode=args.sandbox_mode,
382+
preflight_result=runtime_preflight,
363383
event_sink=event_sink,
364384
)
365385
except KeyboardInterrupt:

pithos/repo_runner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ def _safe_repo_name(path: Path) -> str:
6060
return re.sub(r"[^A-Za-z0-9_.-]+", "-", name).strip("-") or "repo"
6161

6262

63+
def allocate_repo_run_dir(results_dir: Path, repo_path: Path) -> Path:
64+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
65+
return results_dir / _safe_repo_name(repo_path.resolve()) / timestamp
66+
67+
6368
def _git_metadata(repo: Path) -> dict[str, Any]:
6469
def run(*args: str) -> str | None:
6570
try:
@@ -485,13 +490,13 @@ async def run_repo_static(
485490
allow_web: bool = False,
486491
collect_advisories: bool = True,
487492
sandbox_mode: str = "docker",
493+
out_dir: Path | None = None,
488494
event_sink: EventSinkLike = NULL_EVENT_SINK,
489495
) -> RepoRunResult:
490496
start = time.time()
491497
repo_path = repo_path.resolve()
492498
backend = create_execution_backend(sandbox_mode)
493-
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
494-
out_dir = results_dir / _safe_repo_name(repo_path) / timestamp
499+
out_dir = out_dir or allocate_repo_run_dir(results_dir, repo_path)
495500
out_dir.mkdir(parents=True, exist_ok=True)
496501

497502
errors: list[str] = []

pithos/runtime_verifier.py

Lines changed: 197 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,46 @@ def status(self) -> str:
4545
return "completed"
4646

4747

48+
@dataclass(frozen=True)
49+
class RuntimePreflightResult:
50+
repo_path: Path
51+
out_dir: Path
52+
profile: AppRuntimeProfile
53+
preflight: dict[str, Any]
54+
elapsed_s: float
55+
56+
57+
def run_runtime_preflight(
58+
*,
59+
repo_path: Path,
60+
profile_path: Path | None = None,
61+
results_dir: Path | None = None,
62+
execute_app: bool = False,
63+
allow_inferred_runtime: bool = False,
64+
provider: str | None = None,
65+
model: str | None = None,
66+
agent_env: dict[str, str] | None = None,
67+
pi_config_dir: Path | None = None,
68+
sandbox_mode: str | None = None,
69+
event_sink: EventSinkLike = NULL_EVENT_SINK,
70+
) -> RuntimePreflightResult:
71+
"""Run runtime setup checks without requiring static triage artifacts."""
72+
return _run_runtime_preflight(
73+
repo_path=repo_path,
74+
profile_path=profile_path,
75+
results_dir=results_dir,
76+
execute_app=execute_app,
77+
allow_inferred_runtime=allow_inferred_runtime,
78+
provider=provider,
79+
model=model,
80+
agent_env=agent_env,
81+
pi_config_dir=pi_config_dir,
82+
sandbox_mode=sandbox_mode,
83+
event_sink=event_sink,
84+
emit_events=True,
85+
)
86+
87+
4888
def run_verify_repo(
4989
*,
5090
triage_path: Path,
@@ -59,14 +99,17 @@ def run_verify_repo(
5999
agent_env: dict[str, str] | None = None,
60100
pi_config_dir: Path | None = None,
61101
sandbox_mode: str | None = None,
102+
preflight_result: RuntimePreflightResult | None = None,
62103
event_sink: EventSinkLike = NULL_EVENT_SINK,
63104
) -> RuntimeVerifyResult:
64105
start = time.time()
65106
triage_file = _resolve_triage_file(triage_path)
66107
repo = (repo_path or _repo_from_summary(triage_file) or Path.cwd()).expanduser().resolve()
67-
profile = load_runtime_profile(repo, profile_path)
68-
out_dir = results_dir or triage_file.parent / "runtime"
108+
out_dir = results_dir or (preflight_result.out_dir if preflight_result else None)
109+
out_dir = out_dir or triage_file.parent / "runtime"
69110
out_dir.mkdir(parents=True, exist_ok=True)
111+
if preflight_result is not None:
112+
execute_app = bool(preflight_result.preflight.get("execute_app", execute_app))
70113
event_sink.emit(
71114
"runtime_verification_started",
72115
{
@@ -77,52 +120,24 @@ def run_verify_repo(
77120
},
78121
stage="runtime",
79122
)
80-
preflight = _environment_preflight(
81-
profile,
82-
execute_app=execute_app,
83-
allow_inferred_runtime=allow_inferred_runtime,
84-
provider=provider,
85-
model=model,
86-
sandbox_mode=sandbox_mode,
87-
)
88-
if execute_app and not preflight["ready"]:
89-
_write_setup_guide(out_dir / "RUNTIME-SETUP.md", profile, preflight)
90-
event_sink.emit(
91-
"artifact_written",
92-
artifact_data(out_dir / "RUNTIME-SETUP.md", artifact_type="runtime_setup"),
93-
stage="runtime",
123+
if preflight_result is None:
124+
preflight_result = _run_runtime_preflight(
125+
repo_path=repo,
126+
profile_path=profile_path,
127+
results_dir=out_dir,
128+
execute_app=execute_app,
129+
allow_inferred_runtime=allow_inferred_runtime,
130+
provider=provider,
131+
model=model,
132+
agent_env=agent_env,
133+
pi_config_dir=pi_config_dir,
134+
sandbox_mode=sandbox_mode,
135+
event_sink=event_sink,
136+
emit_events=False,
94137
)
95-
_write_runtime_artifact(
96-
out_dir / "environment-summary.json",
97-
preflight,
98-
event_sink=event_sink,
99-
stage="runtime",
100-
artifact_type="runtime_environment_summary",
101-
)
102-
103-
agent_config = {}
104-
if execute_app and provider and model:
105-
agent_config = {
106-
"runner": "pi",
107-
"provider": provider,
108-
"model": model,
109-
"pi_config_dir": str(pi_config_dir) if pi_config_dir else None,
110-
"auth_env_keys": sorted((agent_env or {}).keys()),
111-
}
112-
environment = dict(profile.environment)
113-
if preflight["sandbox_backend"] in {"docker", "local"}:
114-
environment["sandbox"] = preflight["sandbox_backend"]
115-
profile = replace(
116-
profile,
117-
environment=environment,
118-
verification={
119-
**profile.verification,
120-
"execute_app": execute_app,
121-
"allow_inferred_runtime": allow_inferred_runtime,
122-
"preflight": preflight,
123-
"agent": {**dict(profile.verification.get("agent") or {}), **agent_config},
124-
},
125-
)
138+
profile = preflight_result.profile
139+
preflight = preflight_result.preflight
140+
repo = preflight_result.repo_path
126141
findings = _load_merged_findings(triage_file)
127142
if finding_ids:
128143
wanted = set(finding_ids)
@@ -211,6 +226,141 @@ def run_verify_repo(
211226
)
212227

213228

229+
def _run_runtime_preflight(
230+
*,
231+
repo_path: Path,
232+
profile_path: Path | None,
233+
results_dir: Path | None,
234+
execute_app: bool,
235+
allow_inferred_runtime: bool,
236+
provider: str | None,
237+
model: str | None,
238+
agent_env: dict[str, str] | None,
239+
pi_config_dir: Path | None,
240+
sandbox_mode: str | None,
241+
event_sink: EventSinkLike,
242+
emit_events: bool,
243+
) -> RuntimePreflightResult:
244+
start = time.time()
245+
repo = Path(repo_path).expanduser().resolve()
246+
profile = load_runtime_profile(repo, profile_path)
247+
out_dir = results_dir or default_results_dir(repo)
248+
out_dir.mkdir(parents=True, exist_ok=True)
249+
if emit_events:
250+
event_sink.emit(
251+
"runtime_preflight_started",
252+
{
253+
"repo": str(repo),
254+
"results_dir": str(out_dir),
255+
"execute_app": execute_app,
256+
},
257+
stage="runtime",
258+
)
259+
preflight = _environment_preflight(
260+
profile,
261+
execute_app=execute_app,
262+
allow_inferred_runtime=allow_inferred_runtime,
263+
provider=provider,
264+
model=model,
265+
sandbox_mode=sandbox_mode,
266+
)
267+
profile = _profile_with_runtime_context(
268+
profile,
269+
preflight,
270+
execute_app=execute_app,
271+
allow_inferred_runtime=allow_inferred_runtime,
272+
provider=provider,
273+
model=model,
274+
agent_env=agent_env,
275+
pi_config_dir=pi_config_dir,
276+
)
277+
_write_runtime_preflight_artifacts(
278+
out_dir,
279+
profile,
280+
preflight,
281+
execute_app=execute_app,
282+
event_sink=event_sink,
283+
)
284+
elapsed = time.time() - start
285+
if emit_events:
286+
event_sink.emit(
287+
"runtime_preflight_finished",
288+
{
289+
"ready": preflight["ready"],
290+
"issues": len(preflight["issues"]),
291+
"elapsed_s": elapsed,
292+
},
293+
stage="runtime",
294+
)
295+
return RuntimePreflightResult(
296+
repo_path=repo,
297+
out_dir=out_dir,
298+
profile=profile,
299+
preflight=preflight,
300+
elapsed_s=elapsed,
301+
)
302+
303+
304+
def _profile_with_runtime_context(
305+
profile: AppRuntimeProfile,
306+
preflight: dict[str, Any],
307+
*,
308+
execute_app: bool,
309+
allow_inferred_runtime: bool,
310+
provider: str | None,
311+
model: str | None,
312+
agent_env: dict[str, str] | None,
313+
pi_config_dir: Path | None,
314+
) -> AppRuntimeProfile:
315+
agent_config = {}
316+
if execute_app and provider and model:
317+
agent_config = {
318+
"runner": "pi",
319+
"provider": provider,
320+
"model": model,
321+
"pi_config_dir": str(pi_config_dir) if pi_config_dir else None,
322+
"auth_env_keys": sorted((agent_env or {}).keys()),
323+
}
324+
environment = dict(profile.environment)
325+
if preflight["sandbox_backend"] in {"docker", "local"}:
326+
environment["sandbox"] = preflight["sandbox_backend"]
327+
return replace(
328+
profile,
329+
environment=environment,
330+
verification={
331+
**profile.verification,
332+
"execute_app": execute_app,
333+
"allow_inferred_runtime": allow_inferred_runtime,
334+
"preflight": preflight,
335+
"agent": {**dict(profile.verification.get("agent") or {}), **agent_config},
336+
},
337+
)
338+
339+
340+
def _write_runtime_preflight_artifacts(
341+
out_dir: Path,
342+
profile: AppRuntimeProfile,
343+
preflight: dict[str, Any],
344+
*,
345+
execute_app: bool,
346+
event_sink: EventSinkLike,
347+
) -> None:
348+
if execute_app and not preflight["ready"]:
349+
_write_setup_guide(out_dir / "RUNTIME-SETUP.md", profile, preflight)
350+
event_sink.emit(
351+
"artifact_written",
352+
artifact_data(out_dir / "RUNTIME-SETUP.md", artifact_type="runtime_setup"),
353+
stage="runtime",
354+
)
355+
_write_runtime_artifact(
356+
out_dir / "environment-summary.json",
357+
preflight,
358+
event_sink=event_sink,
359+
stage="runtime",
360+
artifact_type="runtime_environment_summary",
361+
)
362+
363+
214364
def _write_runtime_artifact(
215365
path: Path,
216366
data: Any,

0 commit comments

Comments
 (0)