openlatch-client 0.3.3

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
# Egress and corporate proxies

Everything the client sends outward — cloud forwarding, policy-bundle sync, update checks and downloads, telemetry, and the model boundary's upstream call — goes through one egress route. Loopback never does: the hook's POST to the daemon and the CLI's calls to the local admin endpoints bypass the proxy unconditionally, along with anything `no_proxy` matches.

## Zero-config on most corporate machines

`openlatch init` finds the route on its own where it can: it probes what is already configured, then walks the OS's own settings (Windows WinINET/WinHTTP and PAC, macOS `SCDynamicStore` and CFNetwork, GNOME `gsettings`), and only asks — once, on a terminal, unless `--yes` was passed — when none of them reach the platform. A headless failure exits `1` with a JSON report of every candidate it tried. Whatever it finds is written to `[proxy]` in `config.toml`:

```toml
[proxy]
mode = "auto"                       # auto | manual | direct
url = "http://proxy.corp:8080"      # http, https, socks5, socks5h — never userinfo
auth = "auto"                       # auto | none | basic | negotiate
no_proxy = ".corp.example,10.0.0.0/8"
ca_bundle = "/etc/ssl/corp-root.pem"  # merged on top of the OS roots
allow_direct = true                 # false = never fall back to a direct connection
# source is written by discovery — do not hand-edit it
```

Three consequences worth knowing before you deploy at scale:

- **Whatever made `init` work is written down.** The daemon runs under a supervisor with none of your shell's variables, so a route that lived only in `OPENLATCH_PROXY` would leave the daemon going direct an hour later. `[proxy] url` and `[proxy] source` record it.
- **Credentials never reach `config.toml`.** The `[proxy]` table has no password key: the value goes to the OS keychain, with an encrypted-file fallback on headless hosts, and every place a proxy URL is printed or logged is masked as `user:*****@host`.
- **Enabling the model boundary adds loopback `NO_PROXY` entries to your agent's config**, and leaves them behind on teardown. [configuration.md](configuration.md#what-the-daemon-writes-into-the-agents-config) explains why.

## Commands

`openlatch init` takes the route as flags — `--proxy`, `--ca-bundle`, `--no-proxy`, `--proxy-mode`, `--proxy-auth`, `--proxy-spn`, `--dry-run` — listed in [cli.md](cli.md#init-flags). After install:

| Command | What it does |
| ------- | ------------ |
| `openlatch proxy status` | The egress route in effect, in both contexts. Works with the daemon down. |
| `openlatch proxy discover [--force]` | Re-runs the OS discovery ladder, probes each candidate, persists the winner with its `source`. Refuses to overwrite a route you set by hand without `--force`. |
| `openlatch proxy set <url> [--ca-bundle P] [--no-proxy L] [--spn S]` | Pins a route: writes `mode = "manual"` **and** `source = "manual"`, which is what stops discovery and self-heal from replacing it. Prompts for the credential on a `407`. |
| `openlatch proxy clear` | Go direct: `mode = "direct"`, route cleared, credential deleted. |
| `openlatch proxy test [--url T]` | Probes the chain hop by hop. Exit `0` / `7` / `1`. |

`openlatch doctor` names the failing hop in its Connection section — the hop `Cloud` travels over, reported separately so a dead proxy and a dead platform cannot render as the same failure.

## The `[proxy]` table

Resolved per key, environment beating file. The environment equivalents (`OPENLATCH_PROXY` and friends) and where the standard `https_proxy` family sits in the ladder are in [configuration.md](configuration.md#environment-variables).

| Key | Meaning |
| --- | ------- |
| `mode` | `auto` walks the resolver ladder, `manual` uses exactly what is configured and is never overwritten, `direct` never uses a proxy. |
| `url` | `http://`, `https://`, `socks5://` or `socks5h://`. Never carries a username or password — a password in a plaintext file on disk is refused at parse. |
| `username` | The account to authenticate as. |
| `auth` | `auto` probes the proxy once at start-up and picks a scheme; `basic`, `negotiate` and `none` are explicit and skip the probe. NTLM is deliberately absent. |
| `no_proxy` | Additive bypass list, Go grammar (`example.com`, `.example.com`, `10.0.0.0/8`, `*`). |
| `ca_bundle` | A PEM bundle merged **on top of** the OS trust store, for a TLS-inspecting proxy whose CA is not installed system-wide. Never a replacement. |
| `allow_direct` | `false` refuses to fall through to an unproxied connection when no proxy resolves. |
| `spn` | Kerberos service principal override. Defaults to `HTTP/<proxy-host>`. |
| `http1_only` | Force HTTP/1.1 for an inspection proxy that mishandles HTTP/2. |
| `pac_url` | An explicit PAC URL, handed to the OS resolver (WinHTTP, CFNetwork). Refused on Linux with `OL-1225`: this client ships no JavaScript engine. |

## Kerberos / Negotiate

Negotiate rides the `proxy-negotiate` feature, which every released binary ships. It uses the logged-on identity — SSPI on Windows, GSSAPI on macOS and Linux, loaded at first use so a host without Kerberos still starts and still reports why the transport is unavailable. NTLM is refused rather than fallen back to: Microsoft deprecated it in 2024, and a client that quietly downgrades to it is doing the opposite of what it was deployed for.

**On a Kerberos-only proxy, crash reports do not get through.** The Sentry SDK takes a proxy *URL* and nothing else, so there is no place to hand it a SPNEGO token; on an estate whose proxy accepts only `negotiate`, the client leaves those options unset, logs a warning, and everything else keeps working. Crash reporting is optional traffic and this is a deliberate trade, not a defect. The blocking status probe degrades the same way for the same reason — `openlatch status`'s cloud check falls back, `openlatch doctor` still reports through the async path.

## Egress requirements

What to give a network team. Everything but the first row is optional and has a switch that turns it off.

<!-- CANONICAL COPY. This table is the ONE sanctioned duplicate of the "Customer
     egress requirements table" in the Proxy Support PRD's frozen contract. On any
     divergence the PRD wins — change it there first, then mirror it here. Do not
     edit a cell to match observed behaviour. -->

| FQDN | Port | Purpose | Required | Disable switch | TLS inspection stance |
| --- | --- | --- | --- | --- | --- |
| `app.openlatch.ai` | 443 | Policy bundle, event delivery, auth | **Required** | — | Works when the inspection CA is OS-trusted or in `ca_bundle` |
| `api.anthropic.com` | 443 | Model-boundary upstream (the agent's own traffic) | Required when boundary enabled | `[boundary] enabled = false` | **Exempt from response buffering / SSE scanning** — buffered streams break agent responses (OL-1228) |
| `registry.npmjs.org` (or `OPENLATCH_NPM_REGISTRY`) | 443 | Update check + binary download | Optional | `[update]` config | Works with OS-trusted CA |
| PostHog host (`OPENLATCH_POSTHOG_HOST`) | 443 | Product telemetry incl. `proxy_configured` | Optional | Telemetry opt-out (`openlatch telemetry`) | Works |
| Sentry DSN host (`OPENLATCH_SENTRY_DSN`) | 443 | Crash reports | Optional | Crash-report opt-out | Works |
| OpenDNS / Google DNS | UDP 53 | Public-IP detection (enrichment) | Optional | Existing enrichment config | N/A — not HTTP, never proxied |
| `github.com` + release CDN | 443 | npm `postinstall` binary fallback (installer only) | Optional | Pre-install the platform package | npm/curl proxy config applies — outside the client |

Process name for proxy-vendor steering (Netskope/Zscaler custom app definitions): `openlatch` / `openlatch.exe`.

## Troubleshooting

`openlatch doctor` and `openlatch proxy test` both stop at the failing hop and print one of these. The remedy is the code's, not a guess.

| Code | What happened | Remedy |
| ---- | ------------- | ------ |
| `OL-1220` | No proxy is configured and the direct connection to the platform failed at the TCP/DNS level — egress never had a proxy hop to blame | Check outbound DNS resolution and firewall rules for the host running the daemon; or `openlatch proxy set <url>` |
| `OL-1221` | A proxy is configured but did not answer: the TCP connect failed, or the `CONNECT` timed out or was refused | Verify the proxy host and port are correct and reachable from this machine before suspecting the destination |
| `OL-1222` | The proxy answered `407` and either rejected the credentials or had none offered. Distinct from `OL-1201` — that is the platform rejecting our API key; this is the proxy's own gate | Reconfirm the **proxy** credential specifically: `openlatch proxy set <url>`, or check the Kerberos ticket with `klist` |
| `OL-1223` | The proxy URL names a scheme this build cannot speak — anything outside `http`/`https`/`socks5`/`socks5h`, or `negotiate` on a build without the Kerberos feature | Fix the scheme in the proxy URL, or use a build with the feature that scheme requires |
| `OL-1224` | The TLS handshake failed certificate verification. On an enterprise network this is almost always an inspecting proxy re-signing with a CA that is neither in the OS store nor in `ca_bundle` | Point `ca_bundle` at the interception root, or install it in the OS trust store. Never disable verification |
| `OL-1225` | A PAC or WPAD source is configured but the OS could not evaluate it — the PAC URL did not resolve or serve a script, or the platform has no PAC engine | Fix `pac_url`; on Linux, set `[proxy] url` instead. Informational unless the fallback beneath it also fails |
| `OL-1226` | The proxy configuration itself is malformed. Unlike every other code here it **fails startup**: a config the client cannot parse is an authoring bug, not a network state | The message names the exact key or variable — fix that one |
| `OL-1227` | `allow_direct = false` and no usable proxy was found. Egress refuses rather than falling back silently, because on such a network a silent direct connection is an exfiltration path | Fix the proxy configuration, or set `allow_direct = true` if direct egress from this host is acceptable |
| `OL-1228` | The tunnel works but the proxy buffers the stream: an SSE response piles up behind its buffer and lands in one burst. The request still succeeds, so this is a warning | Look for a response-buffering or content-inspection setting on the proxy for the streaming endpoint, and exempt `api.anthropic.com` |
| `OL-1229` | `negotiate` was selected but no usable ticket could be minted — no credential cache, an expired TGT, or an SPN that does not resolve. The proxy was never offered a token at all | Host-side: run `kinit`, confirm the machine is domain-joined, or override the SPN with `--proxy-spn` |
| `OL-1302` | The route in `config.toml` is not the route the running daemon loaded. `proxy set` writes the file; the live process keeps its old route until it restarts — so the proxy you just configured is carrying nothing yet. Reported as a warning, never a pass | `openlatch restart`. If the two still differ afterwards it is not a pending edit but an environment split — on Windows a service account resolves different variables from an interactive shell |