Skip to main content

gaze_proxy/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Multi-provider pass-through proxy for Gaze pseudonymization.
4//!
5//! The proxy does not transcode provider wire formats. Provider adapters only
6//! identify native PII-bearing JSON surfaces; the server applies the supplied
7//! [`gaze::Pipeline`] and session manifest.
8
9pub mod adapter;
10pub mod adapters;
11#[allow(
12    dead_code,
13    reason = "constructors are consumed by the next server integration checkpoint"
14)]
15pub mod codec;
16#[allow(
17    dead_code,
18    reason = "codec probes are consumed by the next server integration checkpoint"
19)]
20pub mod codecs;
21pub(crate) use codecs::anthropic;
22#[cfg(feature = "proxy-daemon")]
23pub mod daemon;
24pub mod error;
25pub mod inspection;
26pub mod principal;
27pub mod server;
28#[allow(
29    dead_code,
30    reason = "finite registry is integrated by the serialized P3/P4 checkpoints"
31)]
32pub(crate) mod session_registry;
33
34use std::net::SocketAddr;
35use std::sync::Arc;
36use std::time::Duration;
37
38use adapters::AnthropicAdapter;
39use gaze::{DictionaryBundle, LocaleChain, LocaleTag, Pipeline};
40
41pub use adapter::{
42    AdapterContract, CoveragePolicy, FormatPolicy, HeaderPolicy, PiiSurface, ProtocolContract,
43    ProviderAdapter, RedirectPolicy, RoutePolicy, SessionPolicy, SessionRegistryConfig,
44    SessionRegistryConfigError, SseEvent, SurfaceSyntax,
45};
46pub use codec::{
47    BodyCodec, CodecError, CodecErrorCode, CodecLimits, CodecPhase, OpaqueCarrierLocation,
48    OpaqueCarrierScheme, OutputProvenance, ProvedRequestBody, ProvedResponseBody,
49    RequestPseudonymizer, RequestTransformContext, ResponseResidualValidator,
50    ResponseTransformContext, WireFormat,
51};
52pub use codecs::anthropic::{
53    AnthropicMessagesCodec, ANTHROPIC_PROXY_ERROR_FRAME, ANTHROPIC_PROXY_PING_FRAME,
54};
55pub use error::{
56    DirectProxyError, HeaderRejectionReason, ProxyError, ProxyErrorCode, ProxyErrorPhase,
57};
58pub use inspection::{install_proxy_inspection_v1, ProxyInspectionProducerV1};
59pub use principal::{
60    AuthenticatedPrincipal, ListenerScope, LocalAuthCredential, PeerScope, PrincipalContext,
61    PrincipalResolveError, PrincipalResolver, ProcessLocalLoopbackResolver,
62    TlsClientCertificateHash, MAX_AUTHENTICATED_PRINCIPAL_BYTES, MAX_LOCAL_AUTH_CREDENTIAL_BYTES,
63};
64pub use server::{
65    classify_upstream_status, parse_request_stream_format, serve, serve_with_listener,
66    validate_declared_body, validate_direct_response_head, DirectResponseHead, HealthSnapshot,
67    DIRECT_PROXY_ERROR_FRAME, DIRECT_PROXY_PING_FRAME,
68};
69
70#[derive(Clone)]
71#[non_exhaustive]
72pub struct ProxyConfig {
73    pub bind: SocketAddr,
74    pub adapters: Vec<Arc<dyn ProviderAdapter>>,
75    pub session_ttl: Duration,
76    pub body_limit_bytes: u64,
77    pub(crate) locale_chain: LocaleChain,
78    pub(crate) dictionaries: DictionaryBundle,
79    pub(crate) direct_anthropic: Option<Arc<AnthropicAdapter>>,
80    pub(crate) inspection: Option<Arc<ProxyInspectionProducerV1>>,
81}
82
83impl ProxyConfig {
84    pub fn new(bind: SocketAddr, adapters: Vec<Arc<dyn ProviderAdapter>>) -> Self {
85        Self {
86            bind,
87            adapters,
88            session_ttl: Duration::from_secs(30 * 60),
89            body_limit_bytes: 2 * 1024 * 1024,
90            locale_chain: default_locale_chain(),
91            dictionaries: DictionaryBundle::default(),
92            direct_anthropic: None,
93            inspection: None,
94        }
95    }
96
97    /// Constructs the strict direct Anthropic profile without a generic trait-object handoff.
98    ///
99    /// This preserves builder-only settings such as continuity, allowlists, timeouts, and an
100    /// optional trusted principal resolver. [`Self::new`] remains source-compatible for legacy
101    /// multi-provider configurations and uses the direct profile's frozen defaults.
102    /// Removing or replacing this adapter in [`Self::adapters`] fails readiness with
103    /// [`ProxyError::DirectAdapterMismatch`], even if the replacement has the same origin.
104    /// Rebuild the configuration with this constructor to change the direct adapter.
105    #[must_use]
106    pub fn anthropic_direct(bind: SocketAddr, adapter: AnthropicAdapter) -> Self {
107        let adapter = Arc::new(adapter);
108        let dynamic: Arc<dyn ProviderAdapter> = adapter.clone();
109        Self {
110            bind,
111            adapters: vec![dynamic],
112            session_ttl: Duration::from_secs(30 * 60),
113            body_limit_bytes: 2 * 1024 * 1024,
114            locale_chain: default_locale_chain(),
115            dictionaries: DictionaryBundle::default(),
116            direct_anthropic: Some(adapter),
117            inspection: None,
118        }
119    }
120
121    /// Sets the locale chain every detection pass on this proxy runs under.
122    ///
123    /// # Why this is explicit
124    ///
125    /// Recognizers declare `locales = [...]` and only run when the active chain intersects
126    /// that list. Before this setting existed the proxy pinned `[LocaleTag::Global]`, so an
127    /// adopter who had already configured `de-DE` silently got no `postal.de` and no DE
128    /// national-phone detection on proxied traffic — an axis-1 leak the adopter had no way to
129    /// observe. The chain is now adopter-supplied rather than inherited from a system default,
130    /// so what activates is a stated configuration decision.
131    ///
132    /// # Derivation
133    ///
134    /// Callers derive the value from the canonical 4-tier chain
135    /// (CLI > policy > rulepack default > system default) with
136    /// [`gaze::LocaleChain::merge_cli_policy_rulepack_default`], or read it straight off a
137    /// `gaze_assembly::CorePipeline::locale_chain`, and pass the result here. It MUST be the
138    /// same chain the supplied [`gaze::Pipeline`] was assembled under: assembly decides which
139    /// recognizers are REGISTERED, this chain decides which of them are allowed to FIRE.
140    ///
141    /// Omitting this call leaves the chain at `[LocaleTag::Global]`, which reproduces the
142    /// pre-existing behavior exactly. [`gaze::LocaleChain`] always appends `Global` as its
143    /// final tier, so configuring a locale can only widen detection, never narrow it.
144    ///
145    /// Both the primary surface pass and the outbound residual re-scan read this one field,
146    /// which is what keeps the residual neither weaker nor stronger than the primary pass.
147    #[must_use]
148    pub fn with_locale_chain(mut self, locale_chain: LocaleChain) -> Self {
149        self.locale_chain = locale_chain;
150        self
151    }
152
153    /// Returns the locale chain detection runs under on this proxy.
154    #[must_use]
155    pub fn locale_chain(&self) -> &[LocaleTag] {
156        self.locale_chain.as_slice()
157    }
158
159    /// Sets the dictionary values every primary and residual detection pass uses.
160    ///
161    /// Omitting this call preserves the pre-existing empty-bundle behavior. The
162    /// same immutable bundle is shared across request protection and residual
163    /// validation so the fail-closed pass cannot know fewer terms than the
164    /// primary pass.
165    #[must_use]
166    pub fn with_dictionaries(mut self, dictionaries: DictionaryBundle) -> Self {
167        self.dictionaries = dictionaries;
168        self
169    }
170
171    /// Returns the dictionary values used by every proxy detection pass.
172    #[must_use]
173    pub fn dictionaries(&self) -> &DictionaryBundle {
174        &self.dictionaries
175    }
176
177    /// Installs the immutable provider-neutral inspection producer for this launch.
178    ///
179    /// The producer can be created only by [`install_proxy_inspection_v1`], which atomically
180    /// binds it to the adopter-supplied pending consumer half. Omitting this method constructs no
181    /// inspection registration or runtime.
182    #[must_use]
183    pub fn with_inspection(mut self, inspection: ProxyInspectionProducerV1) -> Self {
184        self.inspection = Some(Arc::new(inspection));
185        self
186    }
187}
188
189/// The locale chain used when an adopter configures none.
190///
191/// Deliberately `[LocaleTag::Global]`: it is exactly the chain the proxy pinned before
192/// [`ProxyConfig::with_locale_chain`] existed, so adopters who configure nothing observe no
193/// behavior change. Widening this default would activate `postal.us` / `postal.de` for every
194/// adopter at once and is a separate decision (values-vs-bounds).
195fn default_locale_chain() -> LocaleChain {
196    LocaleChain::from_tags(vec![LocaleTag::Global])
197}
198
199pub async fn serve_foreground(
200    config: ProxyConfig,
201    pipeline: Arc<Pipeline>,
202) -> Result<(), ProxyError> {
203    serve(config, pipeline).await
204}