Repository navigation
core: propose HTTP request body observation via http_observer - #339
Open
ghazariann wants to merge 3 commits into
Open
ghazariann wants to merge 3 commits into
ghazariann wants to merge 3 commits into
Conversation
ghazariann
marked this pull request as ready for review
November 1, 2026 05:52
congwang-mk
reviewed
Nov 2, 2026
congwang-mk
left a comment
Contributor
There was a problem hiding this comment.
Thanks for the proposal. The use case makes sense and the hook does have to live in core, but I'd like a different shape:
- Don't buffer.
collect()reads the whole body into supervisor memory before the cap applies, so a sandboxed child can OOM the supervisor. It also turns every upload into a buffered one and makes denied requests read their body before the 403. Tee chunks to the observer as they stream through instead; no cap needed. - One observer, not two. Replace
http_log_fnwith a single callback taking an event struct (method, host, path, headers, ACL verdict, optional body chunks). Pre-1.0, so no need to keep both. Headers matter: withoutContent-Encodinga gzipped body is unreadable.
Response bodies can follow the same tee approach later without breaking SSE.
Contributor
|
@ghazariann any update? thanks. |
Add http_body_fn, an optional proxy callback that receives the request body alongside method, host and path. None by default. It runs before the ACL check and caps what the callback sees at 10 MiB. Signed-off-by: Vahagn <wanghang25@mails.tsinghua.edu.cn>
Collapse http_log_fn and http_body_fn into http_observer, one optional callback given the request method, host, path, headers and ACL verdict. The callback can return a sink, a closure handed each body chunk as the proxy forwards it. The body is teed rather than collected, so the proxy holds one chunk instead of the whole body. Move learn to the new callback, add a test that a 1 MB body reaches the sink as several frames, and update sandbox-reference.md. Signed-off-by: Vahagn <wanghang25@mails.tsinghua.edu.cn>
ghazariann
force-pushed
the
feat/http-body-intercept
branch
from
November 11, 2026 04:02
403795a to
5094bf3
Compare
Add a response slot to HttpSinks, next to the request sink the observer already returns. It holds a hook called once the status and headers exist, which may then return a sink to tee that response's body. TeeBody is reused unchanged for that direction. Re-export HeaderMap so an observer can name the type without depending on http, and add a test that the hook sees the status and its sink receives the body. Signed-off-by: Vahagn <wanghang25@mails.tsinghua.edu.cn>
Contributor
Author
|
The Rust musl (x86_64) failure seems to be unrelated to this change: apt-get could not resolve the Ubuntu mirrors, so it just needs a re-run. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opening this as a proposal rather than a merge request.
Replaces the
log_fnadded in #197 with a single observation callback on the transparent proxy. It receives the request method, host, path, headers and the ACL verdict, and can opt in to the request body as it streams. A response slot does the same for the upstream response, once its status and headers are known.Updated per the review: the body is teed chunk by chunk instead of buffered, and the two callbacks are collapsed into one. The response direction uses the same tee, so it is covered here rather than left as a follow-up.
New API
http_observer(Arc<dyn Fn(&HttpRequestEvent) -> HttpSinks + Send + Sync>)HttpSinkscarries an optional request sink and an optional response hook; empty observes metadata only. Spawns the proxy even without ACL rules, the same wayhttp_log_fndid.No new CLI flags. Off by default (
None).The observer runs after the ACL check so it can report the verdict, and every request that reaches the proxy is observed, including ones the ACL goes on to block.
Rather than carrying
Option<&[u8]>in the event, the callback returns a sink. That gives correlation and end of stream for free: the sink belongs to one request, and it is dropped when that request's body ends, so a consumer can finalize fromDropwithout a second callback.Why this is useful
One possible usage is in downstream crates: the HTTP request body of the child application can be intercepted to give more observability, for example access to each prompt an agent makes.
Discussion
Does this belong in core? Honestly, this is not needed for
learnas a test-before-run tool. Where it is useful islearnused purely as an observability tool, to see what resources a child process accessed and what syscalls and HTTP calls it made. It has to live in core either way, since the body only exists insideAclService::handleand a downstream crate has no way to reach it. Is this too divergent from agent sandboxing, and does it make the codebase less focused?Response bodies:: covered here, using the same tee. The response slot holds a hook rather than a sink because a response's status and headers do not exist when the observer runs, so that metadata has to arrive later: the hook is called once, when it does, and may then return a sink or decline. A consumer needs that before the first chunk, not with it, to build a streaming decoder from
Content-Encoding, to route or skip byContent-Type, or to keep failures and drop large successful bodies. A request the ACL denied has no upstream response, so the hook is never called for one, though the child still receives sandlock's 403.I know this complicates the API. The observer now returns a struct whose response slot is a closure that returns another closure. Is there a better shape for the response direction?
Tests
test_http_observer_records_requests: the observer records method, host and path for a forwarded requesttest_http_observer_sink_receives_body: the sink receives the exact bytes the child sent, and the upstream server receives the same unmodified bytestest_http_observer_reports_denied_request: a request the ACL blocks is reported withallowedfalse and still returns 403test_http_observer_sink_receives_streamed_chunks: a 1 MB body reaches the sink as several frames, summing to the full body, and the upstream receives all of ittest_http_observer_response_hook_tees_body: the hook sees the upstream status and its sink receives the response body🤖 Generated with Claude Code