feat(sandbox): proxy_injection — route proxy-unaware JVM/Node tools through the omac proxy#122
feat(sandbox): proxy_injection — route proxy-unaware JVM/Node tools through the omac proxy#122nhuelstng wants to merge 5 commits into
Conversation
6ce1900 to
4f4abcf
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new sandbox-profile option, network.proxy_injection, to route selected proxy-unaware toolchains through omac’s existing filtering proxy so they participate in the allowlist + interactive network prompt under network.mode=filtered.
Changes:
- Extends the sandbox profile schema/validation with
network.proxy_injectionplus helpers/constants for supported families (jvm,node). - Implements a
sandboxruninjector registry that derives the required env vars for each family (notablyJAVA_TOOL_OPTIONSfor JVM andNODE_USE_ENV_PROXY=1for Node). - Wires proxy injection into
sandboxrun.Run()for filtered runs and adds unit tests to prevent registry/profile drift.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/sandboxrun/run.go | Merges per-family proxy injection env into the child env during filtered runs and emits a notice. |
| internal/sandboxrun/proxyinject.go | New injector registry + env merge helper; implements JVM/Node proxy injection logic. |
| internal/sandboxrun/proxyinject_test.go | Tests injector coverage, merged env output, and JVM tool options rendering/validation. |
| internal/sandboxprofile/profile.go | Adds network.proxy_injection field, supported-family constants, and validation. |
| internal/sandboxprofile/profile_test.go | Adds validation coverage and HasProxyInjection behavior tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Reviewing |
|
Adversarial review plus fresh empirical validation against an anonymized private Gradle build found three blocking gaps:
The dependency-routing mechanism is demonstrably useful, but I recommend fixing these before calling Gradle/Node support complete. Would you prefer to address the three points in this PR, or ship the narrow routing improvement with corrected claims and track the unsupported paths as follow-ups? |
|
Addendum to the review above:
These should be addressed in the implementation/documentation decision together with the three issues above. |
…ir, docs What - macOS/Seatbelt: honor `network.enforcement: env-only` under filtered mode by emitting `(allow network*)` instead of `(deny network*)` + proxy-port-only, mirroring the Linux escape hatch. - proxy_injection node: detect the runtime Node version and warn instead of claiming routing when Node < 24 (or node is absent); NODE_USE_ENV_PROXY is a no-op there, so the previous log overclaimed. - macOS child cwd: set the child working directory on the exec (`ExecWithEnv(..., workdir, ...)`) so `--workdir` is honored. Linux got this via bwrap `--chdir`; Seatbelt has no equivalent, so `./gradlew` from another launch dir was left unresolved. - Docs: document `network.proxy_injection` in the README and the OpenSpec sandbox-profile schema; note the deliberate supervisor-controlled `JAVA_TOOL_OPTIONS`/`NODE_USE_ENV_PROXY` injection exception in the process-isolation spec (still dropped ambient, overlaid by the supervisor). README also corrects the Foojay toolchain claim (routes but 407s on the core-JDK client — authenticated toolchain provisioning is out of scope). Why Adversarial review (NoRiceToday) found the env-only fallback, the Node routing claim, the macOS workdir divergence, and the documentation gaps to be real. (The Copilot nil-pointer report was a false positive — Go's url.Userinfo methods are nil-safe — so no change there.) How - generateSBPLNetwork: early-return allow-all for filtered+env-only. - proxyinject.go: nodeProxyEnvSupported (pure parse) + detectNodeProxySupport (probes `node --version`); run.go warns and drops node from the routed claim when unsupported. - launcher.go: extract newChildCmd and thread workdir into cmd.Dir; ExecWithReady passes "" (cwd inherited, unchanged). Verification - TDD (red→green): TestSBPLNetworkFilteredEnvOnly, TestNodeProxyEnvSupported, TestNewChildCmdSetsWorkdir, TestExecWithEnvRunsInWorkdir. - go build/vet/test ./... green on linux; cross-build+vet green for darwin. Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
…proxy_injection) What - Add a network.proxy_injection profile option (values: "jvm") that routes proxy-unaware toolchains through the omac filtering proxy. - For "jvm": inject a controlled JAVA_TOOL_OPTIONS with the proxy system properties, so *every* JVM launched in the sandbox — Gradle, Maven, sbt, Kotlin, plain java — dials the proxy. One mechanism, no per-tool config files, no GRADLE_USER_HOME redirect. Why - The JVM ignores HTTP(S)_PROXY, so under a filtered sandbox JVM build tools never reach the proxy: their direct connections are blocked by the kernel before the filter runs, so no allow/deny prompt can ever fire and the build fails with an opaque BindException/"Permission denied". - proxy_injection makes that traffic flow through the existing proxy, so the network allowlist and the interactive prompt apply to it — no interception machinery, no new privilege, and no new secret exposure (the proxy token is already in the child's HTTP_PROXY env; the JVM needs it in its own config to satisfy the CONNECT proxy-auth). How - sandboxprofile: Network.ProxyInjection + HasProxyInjection + validation. - sandboxrun.JVMProxyToolOptions: pure generator (proxy URL -> JAVA_TOOL_OPTIONS). - run.go: on filtered runs, inject JAVA_TOOL_OPTIONS when proxy_injection includes "jvm" (the FilterEnv blocklist drops any ambient/attacker value; omac's controlled value is added). Verification - Unit tests for the generator (credentials, no-credentials, invalid URL) and profile validation/accessor. - Manual: real Gradle 9.1 in `omac sandbox run` resolves from Maven Central through the proxy (net ALLOW ...) with no wrapper, and the interactive prompt fires for a non-allowlisted repository — where before it failed silently. Note - The JVM prints a one-line "Picked up JAVA_TOOL_OPTIONS: ..." notice (containing the token) to stderr on each launch. The token is ephemeral, proxy-scoped, and already present in HTTP_PROXY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
What - Generalize network.proxy_injection from a JVM-only special case into a registry of tool families. Add a "node" family alongside "jvm". - jvm -> controlled JAVA_TOOL_OPTIONS (unchanged behaviour). - node -> NODE_USE_ENV_PROXY=1, so Node's built-in fetch/http (undici) honors the already-injected HTTP(S)_PROXY. Requires Node >= 24; older runtimes ignore the var (no-op). The npm/yarn/pnpm CLIs already honor the proxy env and need no family. Why - Most toolchains (curl, git, pip, npm, go) already honor HTTP(S)_PROXY; only families that ignore it need injection. A registry makes adding the next such family a single entry instead of new run.go/schema branches, which keeps the eventual transparent-interception restructuring cheap. How - sandboxprofile: ProxyInjectionTools() is the single source of truth for valid family names; validation keys off it. Add ProxyInjectNode const. - sandboxrun/proxyinject.go: proxyInjectors registry (family -> pure func(proxyURL) (map[string]string, error)) + ProxyInjectionEnv() merger. - run.go: merge ProxyInjectionEnv() for all configured families into the injected child env (FilterEnv drops ambient values; omac's win). Verification - go test ./internal/sandboxrun/ ./internal/sandboxprofile/ (green). - New tests: ProxyInjectionEnv (jvm+node, unknown family) and TestProxyInjectorsCoverProfileTools (guards profile/registry drift). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
The JAVA_TOOL_OPTIONS doc claimed every JVM tool (incl. plain java) authenticates to the proxy. Only tools that parse proxyUser/proxyPassword themselves (Gradle, Maven) do; the core JDK HTTP clients ignore them and 407 against the CONNECT tunnel. Host/port routing still applies to all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
The JDK has no https.nonProxyHosts property — http.nonProxyHosts governs both schemes. Emit it once instead of a per-scheme value the JVM ignores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
…ir, docs What - macOS/Seatbelt: honor `network.enforcement: env-only` under filtered mode by emitting `(allow network*)` instead of `(deny network*)` + proxy-port-only, mirroring the Linux escape hatch. - proxy_injection node: detect the runtime Node version and warn instead of claiming routing when Node < 24 (or node is absent); NODE_USE_ENV_PROXY is a no-op there, so the previous log overclaimed. - macOS child cwd: set the child working directory on the exec (`ExecWithEnv(..., workdir, ...)`) so `--workdir` is honored. Linux got this via bwrap `--chdir`; Seatbelt has no equivalent, so `./gradlew` from another launch dir was left unresolved. - Docs: document `network.proxy_injection` in the README and the OpenSpec sandbox-profile schema; note the deliberate supervisor-controlled `JAVA_TOOL_OPTIONS`/`NODE_USE_ENV_PROXY` injection exception in the process-isolation spec (still dropped ambient, overlaid by the supervisor). README also corrects the Foojay toolchain claim (routes but 407s on the core-JDK client — authenticated toolchain provisioning is out of scope). Why Adversarial review (NoRiceToday) found the env-only fallback, the Node routing claim, the macOS workdir divergence, and the documentation gaps to be real. (The Copilot nil-pointer report was a false positive — Go's url.Userinfo methods are nil-safe — so no change there.) How - generateSBPLNetwork: early-return allow-all for filtered+env-only. - proxyinject.go: nodeProxyEnvSupported (pure parse) + detectNodeProxySupport (probes `node --version`); run.go warns and drops node from the routed claim when unsupported. - launcher.go: extract newChildCmd and thread workdir into cmd.Dir; ExecWithReady passes "" (cwd inherited, unchanged). Verification - TDD (red→green): TestSBPLNetworkFilteredEnvOnly, TestNodeProxyEnvSupported, TestNewChildCmdSetsWorkdir, TestExecWithEnvRunsInWorkdir. - go build/vet/test ./... green on linux; cross-build+vet green for darwin. Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
321becf to
1b7d9d0
Compare
|
Thanks — addressed all five in this PR (rebased onto main,
Re your question: went with addressing the points in-PR rather than deferring, except #1 which stays out of scope with corrected claims. |
What
Adds a
network.proxy_injectionsandbox-profile option that routes proxy-unaware toolchains through omac's existing filtering proxy, so their traffic is subject to the network allowlist and interactive prompt like everything else.The option takes a list of tool families. Each family maps to a small env-injection that points that toolchain at the proxy. Two are supported today:
jvmJAVA_TOOL_OPTIONS(proxy sysprops + CONNECT-auth credentials)javaare routed — proxy auth is tool-dependent, see the auth note belownodeNODE_USE_ENV_PROXY=1fetch/http(undici); Node ≥ 24Adding the next proxy-unaware family is a single entry in the
sandboxruninjector registry — no changes torun.goor the profile schema.Why
Most tools already work under a filtered sandbox —
curl,git,pip,npm/yarn/pnpm, andgoall honorHTTP_PROXY/HTTPS_PROXY, so their traffic already flows through the omac proxy and hits the allowlist + prompt. This PR is only for the families that ignore those env vars:HTTP_PROXY/HTTPS_PROXYentirely. Under a filtered sandbox a JVM build tool opens a direct connection, which the kernel (Landlock) blocks before the proxy filter runs — so the allow/deny prompt never fires and the build dies with an opaquejava.net.BindException: Permission denied. (Full analysis in Network prompt unusable for proxy-unaware clients (JVM/gradle/maven): no popup, silent Landlock block; proxy-configured → 407; shown dialogs self-close #119.)fetch) ignores proxy env unless opted in, so scripts/tools that use nativefetchbypass the proxy the same way. (The npm/yarn/pnpm CLIs are not affected — they already honor the proxy env.)proxy_injectionmakes that traffic flow through the existing proxy. Deliberately the simple fix:HTTP_PROXYenv; the JVM just needs it in its own config to satisfy the proxy'sCONNECTauth (a bareproxyHost/Portotherwise 407s).nodereuses the injectedHTTP_PROXYas-is.How
sandboxprofile:Network.ProxyInjectionlist +ProxyInjectionTools()(the single source of truth for valid family names) + validation.sandboxrun/proxyinject.go: aproxyInjectorsregistry mapping each family to a purefunc(proxyURL) (map[string]string, error), plusProxyInjectionEnv()that merges the env for all configured families.jvm→JAVA_TOOL_OPTIONS(proxy host/port, userinfo credentials, andjdk.http.auth.tunneling.disabledSchemes=to re-enable Basic auth on HTTPS CONNECT).node→NODE_USE_ENV_PROXY=1.run.go: on filtered runs, mergesProxyInjectionEnv()into the injected child env.FilterEnv's blocklist drops any ambient/attackerJAVA_TOOL_OPTIONS/NODE_*; omac's controlled values are then overlaid and win.Verification
Automated (
go test ./internal/sandboxrun/ ./internal/sandboxprofile/):JVMProxyToolOptions: with credentials, without, invalid-URL.ProxyInjectionEnv: jvm + node together; unknown family rejected.TestProxyInjectorsCoverProfileTools: guards against drift between the profile's valid set and the injector registry.HasProxyInjectionaccessor.Manual — real Gradle 9.1 in
omac sandbox runwithproxy_injection: ["jvm"], no wrapper:proxy_injection: jvm routed through the omac proxy.net ALLOW repo.maven.apache.org:443 (allow_domain).BindException, no prompt).Guide — make JVM / Node builds work in the omac sandbox
Where this is configured: everything below lives in the sandbox profile —
~/.config/omac/sandbox-profiles/<profile>.json(default:default.json). It is not in the omacconfig.yaml; that file only selects which profile is active (--profile default). There is no--enforcementrun flag —enforcementis a profile field.Enable injection in your profile. Only list families that ignore
HTTP_PROXY—npm,pip,git,go,curlneed nothing here:Run — no wrapper, no manual proxy config:
Non-allowlisted hosts raise the usual network prompt; allowed ones resolve normally.
What each family covers
jvm— one mechanism for the whole toolchain (no per-tool config files, noGRADLE_USER_HOMEredirect). Host/port routing applies to every JVM: Gradle, Maven, sbt, Kotlin, plainjava. Proxy authentication — the token theCONNECTtunnel requires — is only sent by tools that parse theproxyUser/proxyPasswordsysprops themselves, i.e. Gradle and Maven. Tools that rely on the core JDK HTTP client (HttpURLConnection,java.net.http.HttpClient— e.g. a barejava) are routed through the proxy but won't authenticate and get a 407; bare-JDK proxy auth is out of scope here.node— Node's built-infetch/http(undici). Requires Node ≥ 24; older runtimes silently ignoreNODE_USE_ENV_PROXY(no-op, not an error). The npm/yarn/pnpm CLIs work without this family.curl,git,pip,npm,go, …) already honorsHTTP_PROXYand needs no entry.Gradle specifics
Pick the
enforcementrow that matches how you invoke Gradle — the Gradle daemon is the only reason to relax it (see the daemon note under limitations):./gradlew build(default = daemon)"network": { "enforcement": "env-only" }kernel./gradlew --no-daemon build(ororg.gradle.daemon=false)"enforcement": "kernel"(default)Recommendation: prefer
--no-daemonand keepkernelif you control the invocation; fall back toenv-onlyonly when you can't stop the daemon (the proxy filter + prompt still apply — you only lose the kernel bypass-guarantee).JVM tuning must move to
gradle.properties. Withproxy_injection: ["jvm"]omac ownsJAVA_TOOL_OPTIONS(it's on the always-drop env blocklist and omac overlays a proxy-only value), so heap/encoding/agent flags set viaJAVA_TOOL_OPTIONS/_JAVA_OPTIONSwon't reach the build. Useorg.gradle.jvmargs=…instead.Env not forwarded by default (base passthrough is
PATH, HOME, USER, LANG, LC_ALL, LC_CTYPE, TMPDIR). Add only what your build needs:JAVA_HOME— single-JDK builds are fine viajavaonPATH; add it only for multi-JDK / toolchain-by-JAVA_HOMEsetups (and grant read access to the JDK dir).GRADLE_USER_HOME— unset → cache lands at the sandbox$HOME/.gradle; grant that path read-write or you get a cold cache (full re-download through the proxy) each run.api.foojay.io) toallow_domain.Notes / limitations
Picked up JAVA_TOOL_OPTIONS: …notice containing the proxy token on each launch. The token is ephemeral (per session), proxy-scoped, and already present inHTTP_PROXY; avoid pasting raw build logs publicly.enforcement: "kernel"the Gradle daemon therefore can't connect — run Gradle underenforcement: "env-only"for now (the proxy filter and prompt still apply; only the kernel bypass-guarantee is relaxed — a "trusted build tool" posture). Non-daemon tools (Maven, plainjava, Node) don't hit this and work underkernel.proxy_injectionroutes traffic through the proxy; it does not inject registry credentials. See the note below.Private registry credentials (Maven/npm) — not solved by this PR
Getting the agent to install from an authenticated private registry is a separate concern. Because the build tool runs as the same uid as the agent inside the sandbox, any in-sandbox secret (env,
.npmrc_authToken,settings.xml) is readable by the agent. Two paths:.npmrc/settings.xml) — accepting it is agent-readable, with bounded blast radius.Authorizationupstream, so the sandbox only ever sees a loopback URL + throwaway session token — the same substrate as the Host-side LLM guard-proxy on the harness↔model path (content-level guardrails without TLS MITM) #89 harness↔model guard-proxy. Tracked as a second consumer of Host-side LLM guard-proxy on the harness↔model path (content-level guardrails without TLS MITM) #89, not a separate mechanism.Closes part of #119 (the proxy-unaware-client gap for JVM + Node). Transparent, zero-config, kernel-enforced coverage for all proxy-unaware tools is the transparent-interception transport tracked in #119.
🤖 Generated with Claude Code