Skip to content

feat(sandbox): proxy_injection — route proxy-unaware JVM/Node tools through the omac proxy#122

Open
nhuelstng wants to merge 5 commits into
mainfrom
feat/network-proxy-injection
Open

feat(sandbox): proxy_injection — route proxy-unaware JVM/Node tools through the omac proxy#122
nhuelstng wants to merge 5 commits into
mainfrom
feat/network-proxy-injection

Conversation

@nhuelstng

@nhuelstng nhuelstng commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Adds a network.proxy_injection sandbox-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.

// ~/.config/omac/sandbox-profiles/default.json
"network": {
  "mode": "filtered",
  "proxy_injection": ["jvm", "node"]
}

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:

Family What omac injects Covers
jvm a controlled JAVA_TOOL_OPTIONS (proxy sysprops + CONNECT-auth credentials) Gradle & Maven (routed and authenticated); sbt, Kotlin, plain java are routed — proxy auth is tool-dependent, see the auth note below
node NODE_USE_ENV_PROXY=1 Node's built-in fetch/http (undici); Node ≥ 24

Adding the next proxy-unaware family is a single entry in the sandboxrun injector registry — no changes to run.go or the profile schema.

Why

Most tools already work under a filtered sandboxcurl, git, pip, npm/yarn/pnpm, and go all honor HTTP_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:

proxy_injection makes that traffic flow through the existing proxy. Deliberately the simple fix:

  • No new machinery — reuses the current proxy + filter + prompt.
  • No new privilege — no netns/nftables/interception.
  • No new secret exposure — the proxy token is already in the child's HTTP_PROXY env; the JVM just needs it in its own config to satisfy the proxy's CONNECT auth (a bare proxyHost/Port otherwise 407s). node reuses the injected HTTP_PROXY as-is.

How

  • sandboxprofile: Network.ProxyInjection list + ProxyInjectionTools() (the single source of truth for valid family names) + validation.
  • sandboxrun/proxyinject.go: a proxyInjectors registry mapping each family to a pure func(proxyURL) (map[string]string, error), plus ProxyInjectionEnv() that merges the env for all configured families.
    • jvmJAVA_TOOL_OPTIONS (proxy host/port, userinfo credentials, and jdk.http.auth.tunneling.disabledSchemes= to re-enable Basic auth on HTTPS CONNECT).
    • nodeNODE_USE_ENV_PROXY=1.
  • run.go: on filtered runs, merges ProxyInjectionEnv() into the injected child env. FilterEnv's blocklist drops any ambient/attacker JAVA_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.
  • Profile validation rejects unknown tools; HasProxyInjection accessor.

Manual — real Gradle 9.1 in omac sandbox run with proxy_injection: ["jvm"], no wrapper:

  • omac logs proxy_injection: jvm routed through the omac proxy.
  • Resolution flows through the proxy: net ALLOW repo.maven.apache.org:443 (allow_domain).
  • A non-allowlisted repo raises the network prompt (previously: silent 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 omac config.yaml; that file only selects which profile is active (--profile default). There is no --enforcement run flag — enforcement is a profile field.

  1. Enable injection in your profile. Only list families that ignore HTTP_PROXYnpm, pip, git, go, curl need nothing here:

    // ~/.config/omac/sandbox-profiles/default.json
    {
      "network": {
        "mode": "filtered",
        "proxy_injection": ["jvm", "node"],
        "network_prompt": { "enabled": true },
        // pre-allow repos to skip the per-host prompt (optional):
        "allow_domain": [
          "repo.maven.apache.org", "repo1.maven.org",
          "plugins.gradle.org", "services.gradle.org",
          "registry.npmjs.org"
        ]
      }
    }
  2. Run — no wrapper, no manual proxy config:

    omac start          # or: omac sandbox run -- ./gradlew build   (or: mvn verify, node build.js)
    

    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, no GRADLE_USER_HOME redirect). Host/port routing applies to every JVM: Gradle, Maven, sbt, Kotlin, plain java. Proxy authentication — the token the CONNECT tunnel requires — is only sent by tools that parse the proxyUser/proxyPassword sysprops themselves, i.e. Gradle and Maven. Tools that rely on the core JDK HTTP client (HttpURLConnection, java.net.http.HttpClient — e.g. a bare java) 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-in fetch/http (undici). Requires Node ≥ 24; older runtimes silently ignore NODE_USE_ENV_PROXY (no-op, not an error). The npm/yarn/pnpm CLIs work without this family.
  • Everything else (curl, git, pip, npm, go, …) already honors HTTP_PROXY and needs no entry.

Gradle specifics

Pick the enforcement row that matches how you invoke Gradle — the Gradle daemon is the only reason to relax it (see the daemon note under limitations):

You run Set in profile Why
./gradlew build (default = daemon) "network": { "enforcement": "env-only" } daemon uses a random loopback port Landlock can't allow selectively under kernel
./gradlew --no-daemon build (or org.gradle.daemon=false) keep "enforcement": "kernel" (default) no daemon port → full kernel guarantee retained (preferred)

Recommendation: prefer --no-daemon and keep kernel if you control the invocation; fall back to env-only only 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. With proxy_injection: ["jvm"] omac owns JAVA_TOOL_OPTIONS (it's on the always-drop env blocklist and omac overlays a proxy-only value), so heap/encoding/agent flags set via JAVA_TOOL_OPTIONS/_JAVA_OPTIONS won't reach the build. Use org.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:

"environment": { "allow_vars": ["JAVA_HOME", "GRADLE_USER_HOME", "GRADLE_OPTS"] }
  • JAVA_HOME — single-JDK builds are fine via java on PATH; add it only for multi-JDK / toolchain-by-JAVA_HOME setups (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.
  • Toolchain auto-download resolves through the proxy too — add the download host (e.g. api.foojay.io) to allow_domain.

Notes / limitations

  • Token in stderr (jvm): the JVM prints a one-line Picked up JAVA_TOOL_OPTIONS: … notice containing the proxy token on each launch. The token is ephemeral (per session), proxy-scoped, and already present in HTTP_PROXY; avoid pasting raw build logs publicly.
  • Kernel enforcement + Gradle daemon: the Gradle daemon talks to its client over a random loopback port. Landlock rules are port-based/address-blind, so they can't allow "any loopback port" without also allowing direct egress on those ports (which would defeat the guardrail). Under enforcement: "kernel" the Gradle daemon therefore can't connect — run Gradle under enforcement: "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, plain java, Node) don't hit this and work under kernel.
  • Private/authenticated registries are out of scope here. proxy_injection routes 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:


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

@nhuelstng
nhuelstng force-pushed the feat/network-proxy-injection branch from 6ce1900 to 4f4abcf Compare July 17, 2026 13:55
@nhuelstng nhuelstng changed the title feat(sandbox): proxy_injection — route proxy-unaware JVM tools (Gradle) through the omac proxy feat(sandbox): proxy_injection — route proxy-unaware JVM tools (Gradle/Maven/sbt) through the omac proxy Jul 17, 2026
@nhuelstng
nhuelstng marked this pull request as draft July 17, 2026 14:12
@nhuelstng nhuelstng changed the title feat(sandbox): proxy_injection — route proxy-unaware JVM tools (Gradle/Maven/sbt) through the omac proxy feat(sandbox): proxy_injection — route proxy-unaware JVM/Node tools through the omac proxy Jul 18, 2026
@nhuelstng
nhuelstng marked this pull request as ready for review July 18, 2026 09:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_injection plus helpers/constants for supported families (jvm, node).
  • Implements a sandboxrun injector registry that derives the required env vars for each family (notably JAVA_TOOL_OPTIONS for JVM and NODE_USE_ENV_PROXY=1 for 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.

Comment thread internal/sandboxrun/proxyinject.go
@NoRiceToday

Copy link
Copy Markdown
Contributor

Reviewing

@NoRiceToday

Copy link
Copy Markdown
Contributor

Adversarial review plus fresh empirical validation against an anonymized private Gradle build found three blocking gaps:

  1. JVM injection routes the wrapper, plugin portal, Maven Central, and a private Maven registry through the omac proxy, but a cold Java toolchain download from Foojay fails at proxy authentication. That path uses a core-JDK client which does not send the injected proxy credentials, so a fresh build cannot provision its required toolchain.
  2. On macOS, network.enforcement: env-only is not honored: Seatbelt still emits deny network* and permits only the proxy port. Gradle needs a random loopback socket for its file-lock handler, including with --no-daemon, so the documented fallback does not work. open_port: [0] permits the loopback IPC, but that is not the documented path.
  3. Node <24 silently ignores NODE_USE_ENV_PROXY, while omac logs that the node family is routed through the proxy. The direct request then fails before the proxy/prompt; please detect and warn for unsupported versions rather than claiming routing.

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?

@NoRiceToday

Copy link
Copy Markdown
Contributor

Addendum to the review above:

  1. macOS --workdir behavior differs from Linux. Linux changes the sandbox child CWD to the configured workdir, but the Seatbelt backend does not. Launching from another directory with --workdir <project> leaves relative commands such as ./gradlew unresolved.
  2. network.proxy_injection is not documented in the shipped README or the OpenSpec sandbox-profile schema. The process-isolation spec also still says JAVA_TOOL_OPTIONS is always dropped, without documenting the deliberate supervisor-controlled injection exception.

These should be addressed in the implementation/documentation decision together with the three issues above.

nhuelstng added a commit that referenced this pull request Jul 22, 2026
…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>
nhuelstng and others added 5 commits July 22, 2026 12:54
…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>
@nhuelstng
nhuelstng force-pushed the feat/network-proxy-injection branch from 321becf to 1b7d9d0 Compare July 22, 2026 10:55
@nhuelstng

Copy link
Copy Markdown
Contributor Author

Thanks — addressed all five in this PR (rebased onto main, 1b7d9d0), each with a TDD red→green test.

  1. Foojay toolchain auth — confirmed; out of scope as designed (core-JDK client can't authenticate the CONNECT tunnel → 407). Not code-fixed, but the README no longer implies it "just works" with an allowlist entry — it now states the toolchain download routes but 407s, and authenticated provisioning is deferred to the Host-side LLM guard-proxy on the harness↔model path (content-level guardrails without TLS MITM) #89-based credential-lift proxy.
  2. macOS env-only not honored — fixed. generateSBPLNetwork now emits (allow network*) for filtered+env-only, mirroring the Linux no---enforce path (sbpl.go previously ignored Enforcement). Test: TestSBPLNetworkFilteredEnvOnly.
  3. Node <24 overclaims routing — fixed. omac now probes node --version and, when unsupported/absent, warns and drops node from the "routed" log instead of claiming it. Tests: TestNodeProxyEnvSupported + wiring in run.go.
  4. macOS --workdir no chdir — fixed. The child cwd is now set on the exec (ExecWithEnv(..., workdir, ...)); Linux got this via bwrap --chdir, Seatbelt had no equivalent. Tests: TestNewChildCmdSetsWorkdir, TestExecWithEnvRunsInWorkdir.
  5. Docs gaps — fixed. network.proxy_injection documented in the README and the OpenSpec sandbox-profile schema; the process-isolation spec now notes the deliberate supervisor-controlled JAVA_TOOL_OPTIONS/NODE_USE_ENV_PROXY injection exception (ambient still dropped in layer 1, supervisor value overlaid in layer 3).

Re your question: went with addressing the points in-PR rather than deferring, except #1 which stays out of scope with corrected claims. go build/vet/test ./... green on Linux; cross-build+vet green for darwin.

@nhuelstng
nhuelstng requested review from NoRiceToday and removed request for NoRiceToday July 22, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants