Repository navigation
feat(sandbox): add sandboxed request to Python SDK [replica of harness-sdk PR 4756] - #36
poshinchen wants to merge 4 commits into
Conversation
poshinchen
left a comment
There was a problem hiding this comment.
Strandly v4-reason (optimized) review — run af1746, Opus 4.8
Assessment: comment
Assessment: Comment
Well-structured addition with careful shell quoting, an injection-resistant curl invocation, and strong test coverage (both command-shape mocks and a live curl-against-stdlib-server contract suite). A few items are worth addressing before merge.
Review Categories
- Robustness: The common
timeout=Nonepath passes no curl timeout and no exec timeout, so a stalled connection can hang indefinitely — the most impactful concern. - Security: Temp artifacts live in shared
/tmp; worth confirming the sandbox FS is single-tenant or using a privatemktemp -ddir to avoid symlink redirection. - Documentation: The public
request()Raises:contract omits the synchronous validation errors it can raise; PR description saysfetch()but implementsrequest(). - Testing: Behavioral contract is skipped when
curlis absent — ensure CI guarantees curl. - Nit: Comment typo ("equest").
Nicely considered edge-case handling around redirects, caps, and 304 responses.
| url = url.strip() | ||
| if not _URL_CHARS.fullmatch(url): | ||
| raise SandboxHttpError( | ||
| "request URLs may only contain the characters RFC 3986 allows; " |
There was a problem hiding this comment.
[Important]
Issue: When timeout is None, no --max-time (or --connect-timeout) is passed to curl and exec_timeout is also None, so a stalled connection or a slow-loris server can hang request() indefinitely with no way to recover. The default (most common) call omits timeout, so the default behavior is "no timeout at all".
Suggestion: Consider always passing a --connect-timeout (and/or a sane default overall timeout) even when timeout is None, so the common path is bounded against transport-level hangs. If an unbounded default is intentional, please document that explicitly in the request() docstring and the PR description.
| # the URL reaches a shell command. Security-critical for the curl transport. | ||
| _URL_CHARS = re.compile(r"[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+") | ||
|
|
||
| # Anything outside this set (or a CR/LF/NUL in a value) could inject additional headers |
There was a problem hiding this comment.
[Suggestion]
Issue: Typo in the comment — "smuggle a second equest" should read "request".
Suggestion:
| # Anything outside this set (or a CR/LF/NUL in a value) could inject additional headers | |
| # Anything outside this set (or a CR/LF/NUL in a value) could inject additional headers | |
| # or smuggle a second request, so header names are validated against this pattern. |
| Raises: | ||
| SandboxHttpError: On network/transport errors, or when the response | ||
| body exceeds ``max_bytes``. | ||
| """ |
There was a problem hiding this comment.
[Important]
Issue: The base request() docstring's Raises: section only lists network/transport errors and the max_bytes overflow. The actual implementation also raises SandboxHttpError for input-validation failures (unsupported method, non-http(s) scheme, missing host, URL chars outside the RFC 3986 allowlist, and header name/value injection). Callers reading the public API contract won't know these synchronous validation errors share the same exception type.
Suggestion: Document the validation failures in the Raises: section here (and mirror in the PosixShellSandbox.request docstring), e.g. "SandboxHttpError: On invalid method/URL/headers, network/transport errors, or when the response body exceeds max_bytes."
| """Make an HTTP request with ``curl`` inside the sandbox. | ||
|
|
||
| Works with any HTTP method. HTTP error statuses (4xx/5xx) are returned | ||
| in :attr:`HttpResult.status` rather than raised; only transport-level |
There was a problem hiding this comment.
[Suggestion]
Issue: Temp files are created under the world-writable, shared /tmp with names derived from uuid4(). In a multi-tenant sandbox a pre-existing symlink at /tmp/strands-http-<tag> (or a race) could cause curl's -o/-D writes or the : > body_file truncation to land on an attacker-chosen path. The uuid makes collisions unlikely but doesn't defend against a planted symlink.
Suggestion: Consider writing to a per-request private directory created with restrictive perms (e.g. mktemp -d) rather than individual files in the /tmp root, so a planted symlink can't redirect the writes. If the sandbox's filesystem is already assumed single-tenant/isolated, a short note documenting that assumption would suffice.
| # waiting for a body the server will never send. | ||
| is_head = method == "HEAD" | ||
|
|
||
| # Write the request body to a temp file and feed it via --data-binary so |
There was a problem hiding this comment.
[Suggestion]
Issue: write_file(data_file, ...) for the request body happens before the try/finally that performs the rm -f cleanup. If self.execute(cmd) is never reached but the data file was written (it isn't here since write precedes the try, but note the finally only runs for files created inside the try), the data file is still removed by the finally since its name is in the rm -f list — good. However, if write_file itself raises, no cleanup runs and the body file is already on disk.
Suggestion: Consider moving the body write_file inside the try block so the finally cleanup also covers a partially-written data file on write failure.
| body = self.rfile.read(length) if length else b"" | ||
|
|
||
| if path == "/hello": | ||
| payload = b"<html><body>Hello</body></html>" |
There was a problem hiding this comment.
[Suggestion]
Issue: TestRequestLive is gated on curl being installed (_CURL_AVAILABLE). On CI images without curl, the entire behavioral contract (status/headers/body/resolved_url, cap enforcement, redirects) is silently skipped, leaving only the command-shape mock tests — which can't catch a real curl behavior regression.
Suggestion: Ensure curl is guaranteed present in the CI environment that runs this suite (or make its absence a hard failure there), so the live contract tests can't silently no-op in CI.
|
Live Strandly's actual comments on the original PR 4756 (sanitized copy: mentions escaped, links backticked) Review (CHANGES_REQUESTED):
Review (COMMENTED):
Inline at
Inline at
Inline at
Inline at
Inline at
Inline at
|
Replica of
https://github.com/strands-agents/harness-sdk/pull/4756(backticked: no cross-reference). Below: the v4-reason optimized review and a sanitized copy of live Strandly's actual comments.