Status: R1 + R2 done (validated live against Umbraco). Runtime verification is opt-in (
--runtime, default off) and best-effort (graceful skip when it cannot provision a runnable instance). It never blocks a run and never weakens the static pipeline.
Turn needs_runtime_verification findings (and re-test confirmed ones) into HTTP-level
confirmed / refuted with a real PoC β the class of evidence that separated the reference Umbraco
audit (5 live confirmations: /backofficeHub/negotiate β 200 anon; ?take=2147483647 past model
binding; swagger.json served to anon) from a pure-static tool. It catches guards that exist in
code but aren't wired at runtime, and it raises precision β a finding that won't reproduce live is
honestly downgraded.
Argo's hardest guardrail is "no stage ever contacts, scans, or exercises a live in-scope host." Runtime verification preserves it, by construction:
It never touches the program's real hosts. It builds the open-source target from the already-cloned source into an ephemeral, network-sealed, local instance and probes only that loopback instance β the same trust model a researcher uses (build the OSS app, test your own copy), and the same model
verify.pyalready uses for Docker builds (--network=none).
New explicit invariant: probe targets are loopback-only; the sandbox has no egress; the real in-scope / out-of-scope hosts are never resolved or contacted.
| Risk | Structural prevention |
|---|---|
| Sandbox reaches the internet / the program's prod hosts | container runs egress-blocked (--network=none); probes run inside the container against 127.0.0.1, so no host networking is needed at all |
| A probe aimed at a real host (typo / model error) | deterministic assert_loopback_only(plan, scope) β every target must be loopback; abort if any matches a scope host or resolves off-127.0.0.1 |
DoS vs the local instance (violates prohibited_techniques) |
validate_probe_plan caps request count, RPS, payload size, timeout; rejects fuzz/flood shapes |
| Model runs arbitrary shell | the model never gets a shell or network β it emits a constrained probe plan; a fixed executor runs the validated plan |
| Malicious target source executes locally | build+run inside the sealed container, non-root, --rm, no host mounts beyond the source copy + a result file, no egress |
| Persistent side effects | throwaway copytree + --rm container + ephemeral in-container DB β nothing survives |
orchestrator (host) sealed container (--network=none, --rm, non-root)
isolated copy ββmount roβββΊ /src ββΊ build ββΊ run app on 127.0.0.1:PORT (in-container)
probe_plan.json ββmountβββββΊ /work ββΊ fixed probe-runner curls localhost per the plan
results.json βββmountββββ /work ββ writes {finding_id, request, status, observation}
The orchestrator only ever reads a result file; no port is exposed to the host. Reuses
verify.py (_copy_repo, Docker invocation, timeouts).
- Propose (
04_runtime_probe_prompt.md) β per finding, aruntime_probe_plan: HTTP requests againstlocalhost:PORT+ an expected observation. No shell, no non-loopback hosts. - Validate (deterministic,
guardrails.py) β loopback-only, safe method set, payload/rate/ count/timeout caps. Reject β finding keepsneeds_runtime_verification. - Execute (fixed in-container runner) β runs the validated requests, records status + body snippet.
- Interpret (model, read-only) β returns
runtime: {confirmed|refuted|inconclusive, evidence}, merged into the verdict (theverify.pyon_patchedpattern).
An optional runtime stage after validate (mirrors SCA's opt-out wiring), --runtime
(default off). Per kept finding with a probe plan: build-once β probe β attach a runtime evidence
block; report.py surfaces "β
runtime-confirmed (HTTP PoC)"; benchmark.py folds in
runtime_confirmed_rate (like re_audit_confirmed_rate).
Getting a complex app to actually run is the hard part (Umbraco = .NET 10 SDK + Node 22 + unattended DB). A generic "boot any repo" is not feasible. Pluggable launcher, in priority order:
- User recipe (R3) β explicit
--runtime-image/--runtime-run-cmd, OR a committedargo-runtime.jsonat the repo root ({image | dockerfile, run_cmd?, port?, env?, mount_source?, boot_timeout?}), OR the repo's ownDockerfile(auto-built). Most reliable for real apps. A worked, validated example (the one used for the live Umbraco confirmations) lives at examples/runtime-recipes/umbraco/. - Auto-detect simple stacks (single Flask/Express/FastAPI, static server).
- Graceful skip β no runnable instance β the stage no-ops; findings keep their static verdict.
R2-auth scripts a single (or simple multi-step) cookie login, which covers apps whose session
is established by a username/password POST. It does not cover an OAuth authorization-code +
PKCE flow (e.g. Umbraco's backoffice: POST /login only sets the Identity cookie; the Management
API requires an OpenIddict access token obtained via GET /authorize β POST /token with a
runtime-generated code_verifier/code_challenge and a redirect-extracted code). PKCE values
cannot be expressed as static probe requests (they are generated/parsed at runtime), so the LLM
cannot script them. Confirming such authz findings at runtime would need a dedicated, target-
specific OAuth helper β a brittle, low-ROI effort, since those findings are already confirmed
statically and diffed against the baseline-correct reference. Deliberately not built; authz
findings on OAuth/PKCE backends stay runtime_inconclusive (honestly), not falsely confirmed.
- Provisioning: user-recipe-first + graceful skip (reliable, honest) β not magical auto-boot.
- Docker: required; skip with a clear message if absent.
- Probe scope: read-only by default (GET/HEAD/negotiate, take-overflow); state-changing PoCs only behind an extra opt-in.
- R1 β safe harness, no LLM: sealed-container build+run+probe +
assert_loopback_only+validate_probe_plan+ a hand-written probe plan. β DONE β reproduced the reference's/server/status+/server/configurationanonymous confirmations against a sealed Umbraco. - R2 β LLM probe plans + interpretation β
DONE β
04_runtime_probe_prompt.md(propose, offline, read-only repo) generatesruntime_probe_plan.jsonfrom the validated findings (gated by the R1 validators);05_runtime_interpret_prompt.md(interpret) turns the observations into per-findingruntime_confirmed/refuted/inconclusiveverdicts merged intovalidated_findings.json. A hand-written plan still overrides generation. R2-auth: a finding entry may carry an optionalauthlogin step (used only whenruntime_credentialsare provided) β the probe runner keeps a per-finding cookie jar so the probes run as that session, letting authz findings be runtime-confirmed. The login POST is exempt from the read-only gate (it is non-destructive) but is still loopback-checked and capped; the actual probe methods stay read-only unless explicitly opted in. - R3 β launcher auto-detection β
DONE β
_resolve_launcherresolves provisioning in priority order: explicit--runtime-image/--runtime-run-cmdβ anargo-runtime.jsonrecipe ({image | dockerfile, run_cmd?, port?, env?, mount_source?, boot_timeout?}; adockerfileis auto-built) β the repo's ownDockerfile(auto-built, run via its default CMD, port fromEXPOSE). None β graceful skip. Env injection (-e) and image-default-CMD now supported. - R4 β verdicts in the report β
DONE β each finding's
validation.runtimeverdict + evidence renders inREPORT.md(and confirmed ones in the submission draft); a gated exec-summary line counts runtime-confirmed findings. The golden report stays byte-stable because the runtime block only renders when present (off by default). The benchmarkruntime_confirmed_rateis deferred (it needs a runtime recipe per labeled case β a rare config).
config.py (runtime flags/caps) Β· guardrails.py (assert_loopback_only, validate_probe_plan) Β·
argo/runtime.py (+ reuse verify.py) Β· argo/prompts/04_runtime_probe_prompt.md Β· models.py
(RuntimeEvidence) Β· orchestrator.py + cli.py (optional stage, --runtime, argo runtime) Β·
report.py, benchmark.py, guardrails.md (mandatory safety section), both SVGs.