diff --git a/authbridge/cmd/authbridge-cpex/main.go b/authbridge/cmd/authbridge-cpex/main.go index c788ab5e..db6a8cd1 100644 --- a/authbridge/cmd/authbridge-cpex/main.go +++ b/authbridge/cmd/authbridge-cpex/main.go @@ -24,6 +24,7 @@ import ( "fmt" "log" "log/slog" + "net" "net/http" "os" "os/signal" @@ -319,9 +320,13 @@ func startHTTPServer(name string, handler http.Handler, addr string) *http.Serve Handler: handler, ReadHeaderTimeout: 10 * time.Second, } + listener, err := net.Listen("tcp", addr) + if err != nil { + log.Fatalf("%s listen: %v", name, err) + } go func() { - slog.Info("HTTP server listening", "name", name, "addr", addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Info("HTTP server listening", "name", name, "addr", listener.Addr().String()) + if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("%s serve: %v", name, err) } }() @@ -339,7 +344,7 @@ func startReverseProxyServer(name string, rp *reverseproxy.Server, addr string) log.Fatalf("%s listen: %v", name, err) } go func() { - slog.Info("HTTP server listening", "name", name, "addr", addr, "mtls", rp.MTLSEnabled()) + slog.Info("Reverse server listening", "name", name, "addr", listener.Addr().String(), "mtls", rp.MTLSEnabled()) if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("%s serve: %v", name, err) } diff --git a/authbridge/cmd/authbridge-proxy/main.go b/authbridge/cmd/authbridge-proxy/main.go index 0fa871ff..2da2c840 100644 --- a/authbridge/cmd/authbridge-proxy/main.go +++ b/authbridge/cmd/authbridge-proxy/main.go @@ -544,9 +544,13 @@ func startHTTPServer(name string, handler http.Handler, addr string) *http.Serve Handler: handler, ReadHeaderTimeout: 10 * time.Second, } + listener, err := net.Listen("tcp", addr) + if err != nil { + log.Fatalf("%s listen: %v", name, err) + } go func() { - slog.Info("HTTP server listening", "name", name, "addr", addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Info("HTTP server listening", "name", name, "addr", listener.Addr().String()) + if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("%s serve: %v", name, err) } }() @@ -572,7 +576,7 @@ func startReverseProxyServer(name string, rp *reverseproxy.Server, addr string) log.Fatalf("%s listen: %v", name, err) } go func() { - slog.Info("HTTP server listening", "name", name, "addr", addr, "mtls", rp.MTLSEnabled()) + slog.Info("Reverse server listening", "name", name, "addr", listener.Addr().String(), "mtls", rp.MTLSEnabled()) if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("%s serve: %v", name, err) } diff --git a/authbridge/demos/context-guru/README.md b/authbridge/demos/context-guru/README.md index 66b2b0b4..2996361d 100644 --- a/authbridge/demos/context-guru/README.md +++ b/authbridge/demos/context-guru/README.md @@ -157,3 +157,4 @@ inject a second sidecar). The extract-code key lives in the `cg-model-key` Secre - `k8s/authbridge-config.yaml` — sidecar config (pipeline, engine, the 3 modes). - `k8s/agent.yaml` — agent + context-guru sidecar Deployment/Service. - `run.sh` — `setup` / `drive ` / `all`. +- `context-guru-tls-bridge.yaml` — standalone/laptop HTTPS variant (tls_bridge + HTTPS_PROXY), not used by `run.sh`, but used by a demo in https://github.com/rossoctl/rossoctl diff --git a/authbridge/demos/context-guru/context-guru-tls-bridge.yaml b/authbridge/demos/context-guru/context-guru-tls-bridge.yaml new file mode 100644 index 00000000..8e60a8cb --- /dev/null +++ b/authbridge/demos/context-guru/context-guru-tls-bridge.yaml @@ -0,0 +1,77 @@ +# AuthBridge config for the context-guru demo — HTTPS variant. +# +# Use a `tls_bridge` block so +# the forward proxy MITM-terminates the agent's outbound TLS. Without it, HTTPS +# LLM calls arrive as an opaque CONNECT tunnel and context-guru sees no body. +# With it, the decrypted HTTP request is fed through the outbound pipeline, so +# inference-parser + context-guru can read and rewrite the LLM request body. +# +# ── PREREQUISITES (all three are required for HTTPS interception to work) ── +# 1. Client trusts the bridge CA. TLS termination means the proxy presents its +# OWN leaf cert for the LLM host. The agent's HTTP client rejects it unless +# it trusts /tmp/tls-bridge-ca/ca.crt. For a Node agent: +# NODE_EXTRA_CA_CERTS=/tmp/tls-bridge-ca/ca.crt +# (Python: REQUESTS_CA_BUNDLE / SSL_CERT_FILE; Go: SSL_CERT_FILE.) +# 2. Agent routes HTTPS through the forward proxy: +# HTTPS_PROXY=http://127.0.0.1:8081 +# 3. Plugin is compiled in (it is opt-in) and the package is fully built: +# LOG_LEVEL=debug go run -tags include_plugin_contextguru . +# +# THREE MODES via the context-guru entry's `on_error` (unchanged from the demo): +# enforce (default) — compaction is applied to the outbound request. +# observe — shadow: engine runs and records what it WOULD save, but +# the request is forwarded unchanged. A/B the gain live. +# off — kill-switch: the plugin is dropped entirely (baseline). +mode: proxy-sidecar +listener: + roles: [forward] # egress-only: no reverse proxy at all + forward_proxy_addr: "localhost:8081" + # reverse_proxy_backend: "http://localhost:8001" # the agent (moved off :0) + +# TLS termination of agent egress so the outbound pipeline sees decrypted HTTPS. +tls_bridge: + mode: enabled # disabled | enabled (empty == disabled) + ca_dir: /tmp/tls-bridge-ca # required when enabled; MUST persist across restarts + generate_ca: true # demo/standalone: mint a self-signed CA into ca_dir + # if absent. In-cluster this stays false (CA is a + # mounted cert-manager Secret). + ports: [443, 8443] # ports to intercept as TLS (this is also the default). + # Only HTTP(S)-bearing ports — do NOT add non-HTTP + # TLS ports (LDAPS/DB-over-TLS) or the bridge breaks them. + # upstream_ca_bundle: /path/to/roots.pem # extra roots for re-origination to a + # private-CA LLM endpoint; omit for system roots only. + +pipeline: + outbound: + plugins: + - name: inference-parser + - name: context-guru + on_error: enforce # enforce | observe | off (see header) + config: + # Only inference paths are compacted; everything else passes through. + paths: ["/v1/chat/completions", "/v1/completions", "/v1/messages"] + # Static cheap model for the LLM-backed extract:code component. OpenAI wire + # only (base_url + /v1/chat/completions). Key injected from a Secret via env. + # NOTE: if CG_MODEL_BASE is now an https:// endpoint, this outbound call ALSO + # goes through the forward proxy — its host must be on an intercepted port + # (443/8443) or listed under passthrough_hosts, and its cert must verify. + model: + base_url: "${CG_MODEL_BASE}" + model: "${CG_MODEL_NAME}" + api_key: "${CG_MODEL_KEY}" + # `engine` is context-guru's native config, passed verbatim to config.LoadBytes. + engine: + # 2 deterministic reducers (dedup, collapse) + extract strategy=code. + pipeline: [dedup, extract, collapse] + components: + dedup: # drop byte-identical tool outputs + min_tokens: 40 + extract: # LLM writes a sandboxed, deletion-only, + strategy: code # containment-proven projection of each + marker_mode: summary # large tool output (v1: no restoration -> + model: { source: config } # summary leaves a ⟪cg⟫ breadcrumb, no stash) + trigger: { min_output_tokens: 120 } # only project outputs at least this big + collapse: # gentle head/tail net for anything extract left + max_tokens: 300 + head_lines: 12 + tail_lines: 12