Add reusable proxy env and net wrapper helpers

ober

d661dcbcd6c30c412b855deade6191d5e7218d61

diff --git a/docs/limits-followup.md b/docs/limits-followup.md
index 66fd8d9..41660c6 100644
--- a/docs/limits-followup.md
+++ b/docs/limits-followup.md
@@ -1,7 +1,7 @@
 # Limits Follow-Up: Missing Enforcement and Test Work
 
 Reviewed: 2026-05-21
-Status updated: 2026-05-22 (sandbox denied path phase)
+Status updated: 2026-05-22 (proxy handoff helper phase)
 
 This document is a follow-up to `docs/limits.md` after reviewing the new
 limits/sandbox/audit module set added around commit `97eea41`.
@@ -22,7 +22,7 @@ backend-specific filesystem and network denial tests are still needed.
 | 3  | Resource limits        | SAFE PARTIAL — requested-limit plans and child install reports are per-kind; `require: '(limits)` fails closed before exec; parent-side time/output markers exist; **GAP**: cgroup v2 |
 | 4  | Executable identity    | DONE — path search, realpath/stat identity, comparison helper, and TOCTOU caveat are present |
 | 5  | Filesystem tracing     | SAFE PARTIAL — `tracefs-capabilities`, fail-closed wrapper, normalized `fs-event` output, best-effort fd/cwd tracking, and generic read/write/exec suggestions exist; **GAP**: native backends and deeper Linux traced-run tests |
-| 6  | Network allowlist      | SAFE PARTIAL — reusable decision layer, parent-side DNS resolution, resolved-address localnet recheck, structured proxy events, and binary CONNECT tunneling exist; **GAP**: child network sandbox/proxy handoff and direct-network denial tests |
+| 6  | Network allowlist      | SAFE PARTIAL — reusable decision layer, parent-side DNS resolution, resolved-address localnet recheck, structured proxy events, binary CONNECT tunneling, proxy env construction, and macOS local-only command wrapping exist; **GAP**: full child network sandbox/proxy handoff in consumers and direct-network denial tests |
 | 7  | Environment/secrets    | DONE — default deny policy, argv validation, env construction, and redaction helpers exist |
 | 8  | Temp HOME/cache        | DONE for helper layer — fake HOME, scratch/cache grants, env overrides, cleanup, sandbox grant helper, and reusable named path capability resolution exist |
 | 9  | Structured audit model | PARTIAL — event constructors and redaction exist; **GAP**: full integration from all primitives |
@@ -116,6 +116,9 @@ Implemented:
 - `sandbox-command-wrapper-needed?`, `sandbox-command-wrapper-available?`, and
   `sandbox-wrap-command` expose the reusable command rewrite needed by custom
   launchers that cannot call `sandbox-launch` directly.
+- `net: 'local-only` and `net: 'allowlist` can use the macOS command wrapper
+  path so custom launchers can start a target with remote network denied before
+  the target binary runs.
 - Linux Landlock hook via `jerboa_landlock_sandbox`; policies with negative
   path exceptions report degraded on backends that cannot enforce those
   exceptions.
@@ -150,7 +153,7 @@ Required next work:
    - fail-closed aborts when a required axis is not fully enforced
 2. Split macOS capability reporting so "can wrap target with sandbox-exec" is
    distinguishable from "this already-running child process is confined".
-3. Add a first-class network allowlist/proxy handoff requirement.
+3. Add deeper direct-network denial tests for proxy handoff consumers.
 
 ## 2. Process Supervision
 
diff --git a/docs/limits.md b/docs/limits.md
index d6deb20..33f1fd5 100644
--- a/docs/limits.md
+++ b/docs/limits.md
@@ -85,6 +85,9 @@ report status to the parent through `(std os supervise)` before exec. Policies
 also accept `deny-read-paths:`, `deny-write-paths:`, and `deny-exec-paths:`;
 macOS Seatbelt renders those as explicit SBPL denies, while backends without
 negative path exceptions report degraded so fail-closed callers can refuse.
+`(std net allow-proxy)` provides `allow-proxy-url` and `allow-proxy-env` helpers
+for proxy handoff, and macOS `sandbox-wrap-command` can now wrap `net:
+local-only` policies so custom launchers can deny remote egress before exec.
 
 ## 2. Process Supervision
 
diff --git a/lib/std/net/allow-proxy.ss b/lib/std/net/allow-proxy.ss
index d05043e..3e96a04 100644
--- a/lib/std/net/allow-proxy.ss
+++ b/lib/std/net/allow-proxy.ss
@@ -46,6 +46,8 @@
     allow-proxy-stop!
     allow-proxy-port
     allow-proxy-host
+    allow-proxy-url
+    allow-proxy-env
     allow-proxy-allowlist
     allow-proxy-logger
     allow-proxy-stats
@@ -104,6 +106,27 @@
         [srv (tcp-server-port srv)]
         [else (allow-proxy-rec-port p)])))
 
+  (def (allow-proxy-url p)
+    (string-append "http://"
+                   (allow-proxy-host p)
+                   ":"
+                   (number->string (allow-proxy-port p))))
+
+  (def (allow-proxy-env p)
+    ;; Environment overrides understood by common HTTP clients.  The
+    ;; lowercase spellings are intentional: curl, wget, Python requests,
+    ;; Node, and many package managers check one or both forms.
+    (let ([url (allow-proxy-url p)]
+          [no-proxy "localhost,127.0.0.1,::1"])
+      (list (cons "HTTP_PROXY" url)
+            (cons "HTTPS_PROXY" url)
+            (cons "ALL_PROXY" url)
+            (cons "http_proxy" url)
+            (cons "https_proxy" url)
+            (cons "all_proxy" url)
+            (cons "NO_PROXY" no-proxy)
+            (cons "no_proxy" no-proxy))))
+
   (def (allow-proxy . opts)
     (let ([host "127.0.0.1"] [port 0] [allow '()] [logger #f]
           [allow-ipl? #f] [allow-localnet? #f])
diff --git a/lib/std/os/limits/sandbox.ss b/lib/std/os/limits/sandbox.ss
index 77d78e4..b0cf3aa 100644
--- a/lib/std/os/limits/sandbox.ss
+++ b/lib/std/os/limits/sandbox.ss
@@ -351,15 +351,24 @@
                          fs-status))
         (syscalls . unavailable))))
 
+  (def (sandbox-policy-net-wrapper-needed? pol)
+    ;; `deny` is the neutral default today, so do not force every default
+    ;; policy through sandbox-exec. Callers that need direct-network denial
+    ;; for proxy handoff should request local-only or allowlist explicitly.
+    (case (sandbox-policy-get pol 'net)
+      [(local-only allowlist) #t]
+      [else #f]))
+
   (def (macos-wrap-command pol cmd)
-    ;; If the policy has paths, rewrite CMD to invoke /usr/bin/sandbox-exec
-    ;; with the generated SBPL profile, so the policy is applied BEFORE
-    ;; the target binary starts.
+    ;; If the policy has paths or an explicit wrapper-enforceable network
+    ;; mode, rewrite CMD to invoke /usr/bin/sandbox-exec so the policy is
+    ;; applied BEFORE the target binary starts.
     (let* ([read  (sandbox-policy-get pol 'read-paths)]
            [write (sandbox-policy-get pol 'write-paths)]
            [exec  (sandbox-policy-get pol 'exec-paths)])
       (cond
-        [(sandbox-policy-has-paths? pol)
+        [(or (sandbox-policy-has-paths? pol)
+             (sandbox-policy-net-wrapper-needed? pol))
          (let ([sbpl (build-sbpl read write exec
                                  (sandbox-policy-get pol 'deny-read-paths)
                                  (sandbox-policy-get pol 'deny-write-paths)
@@ -383,9 +392,10 @@
   (def (sandbox-command-wrapper-needed? pol)
     ;; Some backends install policy directly in the child between fork and
     ;; exec. macOS path policies are different: they must exec through
-    ;; sandbox-exec so the target starts inside the deny-default profile.
+    ;; sandbox-exec so the target starts inside the profile.
     (and (platform-macos?)
-         (sandbox-policy-has-paths? pol)))
+         (or (sandbox-policy-has-paths? pol)
+             (sandbox-policy-net-wrapper-needed? pol))))
 
   (def (sandbox-command-wrapper-available? pol)
     (or (not (sandbox-command-wrapper-needed? pol))
diff --git a/tests/test-limits-primitives.ss b/tests/test-limits-primitives.ss
index 04c7847..e9dfa13 100644
--- a/tests/test-limits-primitives.ss
+++ b/tests/test-limits-primitives.ss
@@ -346,6 +346,24 @@
     (alist-ref/default d 'connect-host #f)
     "127.0.0.1"))
 
+(let ([p (allow-proxy 'host: "127.0.0.1" 'port: 12345
+                      'allow: '("example.com:443"))])
+  (test "allow-proxy-url renders host and port"
+    (allow-proxy-url p)
+    "http://127.0.0.1:12345")
+  (test-pred "allow-proxy-env exports HTTPS proxy"
+    (allow-proxy-env p)
+    (lambda (xs)
+      (and (assoc "HTTPS_PROXY" xs)
+           (string=? (cdr (assoc "HTTPS_PROXY" xs))
+                     "http://127.0.0.1:12345"))))
+  (test-pred "allow-proxy-env exports lowercase proxy"
+    (allow-proxy-env p)
+    (lambda (xs)
+      (and (assoc "https_proxy" xs)
+           (string=? (cdr (assoc "https_proxy" xs))
+                     "http://127.0.0.1:12345")))))
+
 (test-pred "net-allowlist decision layer matches wildcard host"
   (net-allowlist-host-allowed? '("*.example.com:443")
                                "api.example.com" 443 #f #f)
@@ -503,6 +521,29 @@
         "/usr/bin/sandbox-exec"
         cmd)))
 
+(let* ([pol (sandbox-policy 'net: 'local-only)]
+       [cmd '("/bin/echo" "net-wrapped?")]
+       [wrapped (sandbox-wrap-command pol cmd)]
+       [sbpl (sandbox-policy-sbpl pol)])
+  (test "sandbox net-local wrapper need is platform-aware"
+    (sandbox-command-wrapper-needed? pol)
+    (platform-macos?))
+  (test "sandbox net-local wrapper changes command only when needed and available"
+    (if (and (sandbox-command-wrapper-needed? pol)
+             (sandbox-command-wrapper-available? pol))
+        (car wrapped)
+        wrapped)
+    (if (and (sandbox-command-wrapper-needed? pol)
+             (sandbox-command-wrapper-available? pol))
+        "/usr/bin/sandbox-exec"
+        cmd))
+  (test-pred "sandbox net-local SBPL denies network first"
+    sbpl
+    (lambda (s) (string-contains? s "(deny network*)")))
+  (test-pred "sandbox net-local SBPL allows local network"
+    sbpl
+    (lambda (s) (string-contains? s "(allow network* (local ip))"))))
+
 (let* ([pol (sandbox-policy
              'write-paths: '("/tmp/project")
              'deny-read-paths: '("/tmp/project/secret")