Filed from a review of inject_probe vs the http_replay engine.
Summary
inject_probe fires a payload battery at one parameter-bearing request, but it does its network I/O through its own send path — raw global fetch(), its own host scope guard, its own timeout/budget — instead of the verified, governed replay engine in src/replay/* that http_replay sits on. This duplicates logic that already exists (and is unit-tested) and leaves the battery ungoverned.
What inject_probe does now
src/tool/inject-probe.ts — internal send() (~line 907) calls the global fetch() (~line 937). It builds a URL + RequestInit rather than an HttpMessage.Request.
- Own scope guard:
guardHost(r, u) (~lines 861-863) plus the in-scope allowlist check (~lines 1690-1706) — parallel to http_replay's inScope (src/tool/http-replay.ts), i.e. the same "only reach a host the crawl already captured" rule, implemented twice.
- Own per-request deadline (
SEND_TIMEOUT_MS, ~line 902) and its own ad-hoc send budget counter.
Why it matters
- No DoS/rate governing on a battery.
http_replay sends go through Send.governed(...) + Governor (budget / circuit-breaker / retry-on-transient). inject_probe fires many payloads per call with none of that — it is exactly the caller that most needs pacing/circuit-breaking, yet has the least.
- Duplication / drift risk. Two scope guards, two timeout policies, two response-extraction paths must be kept in sync; a fix to one (e.g. the fetch-hang deadline) has to be mirrored by hand.
- No byte-exact option.
fetch() normalizes the request; the engine also has BackendSocket (raw TCP/TLS) for payloads that must survive on the wire (relevant to some injection classes).
Proposed approach
Route inject_probe's sends through the same engine primitives http_replay uses (src/replay/*), not the raw fetch():
- Minimal: replace the
fetch() in send() with BackendFetch.send(msg, { origin, totalTimeoutMs, signal }) (src/replay/backend-fetch.ts), building an HttpMessage.Request from the probe.
- Fuller (preferred): wrap it in
Send.governed(() => BackendFetch.send(...), { budget, breaker }) so the battery is paced and circuit-broken, and share inScope instead of guardHost (delete the duplicate). Optionally reuse ../replay/observe for the generic reflection/error-signature facts, keeping inject_probe's specialized observation (tag-survival, SSTI evaluation, cmd-output detection) as-is.
Note: this is not a tool-to-tool call — http_replay's mutation/compare/sweep surface does not fit a payload battery. The shared thing is the send engine, not the tool.
Relation to #91
If the shared send is wired to a session-wide governor (module-level, keyed by session/host), this and #91 (governors instantiated per-call → inert) are naturally addressed together: one governor coordinating http_replay, inject_probe, and the parallel testers is the actual session-wide protection.
Constraints to preserve
- inject_probe stays an evidence engine (facts, no verdict) — its specialized observation and coverage reporting must not change.
- The existing scope-safety guarantee (only hosts the crawl captured) must be preserved exactly (that is the whole point of sharing
inScope).
- inject_probe already accepts a constructed
target: {method, url} — the migration should keep that path working (it parallels the http_replay target source).
Filed from a review of
inject_probevs thehttp_replayengine.Summary
inject_probefires a payload battery at one parameter-bearing request, but it does its network I/O through its own send path — raw globalfetch(), its own host scope guard, its own timeout/budget — instead of the verified, governed replay engine insrc/replay/*thathttp_replaysits on. This duplicates logic that already exists (and is unit-tested) and leaves the battery ungoverned.What inject_probe does now
src/tool/inject-probe.ts— internalsend()(~line 907) calls the globalfetch()(~line 937). It builds aURL+RequestInitrather than anHttpMessage.Request.guardHost(r, u)(~lines 861-863) plus the in-scope allowlist check (~lines 1690-1706) — parallel tohttp_replay'sinScope(src/tool/http-replay.ts), i.e. the same "only reach a host the crawl already captured" rule, implemented twice.SEND_TIMEOUT_MS, ~line 902) and its own ad-hoc send budget counter.Why it matters
http_replaysends go throughSend.governed(...)+Governor(budget / circuit-breaker / retry-on-transient).inject_probefires many payloads per call with none of that — it is exactly the caller that most needs pacing/circuit-breaking, yet has the least.fetch()normalizes the request; the engine also hasBackendSocket(raw TCP/TLS) for payloads that must survive on the wire (relevant to some injection classes).Proposed approach
Route
inject_probe's sends through the same engine primitiveshttp_replayuses (src/replay/*), not the rawfetch():fetch()insend()withBackendFetch.send(msg, { origin, totalTimeoutMs, signal })(src/replay/backend-fetch.ts), building anHttpMessage.Requestfrom the probe.Send.governed(() => BackendFetch.send(...), { budget, breaker })so the battery is paced and circuit-broken, and shareinScopeinstead ofguardHost(delete the duplicate). Optionally reuse../replay/observefor the generic reflection/error-signature facts, keeping inject_probe's specialized observation (tag-survival, SSTI evaluation, cmd-output detection) as-is.Note: this is not a tool-to-tool call —
http_replay's mutation/compare/sweep surface does not fit a payload battery. The shared thing is the send engine, not the tool.Relation to #91
If the shared send is wired to a session-wide governor (module-level, keyed by session/host), this and #91 (governors instantiated per-call → inert) are naturally addressed together: one governor coordinating
http_replay,inject_probe, and the parallel testers is the actual session-wide protection.Constraints to preserve
inScope).target: {method, url}— the migration should keep that path working (it parallels thehttp_replaytargetsource).