Skip to content

Commit 83003e8

Browse files
authored
feat(interceptors): initial gateway interceptor implementation and reference example (#2005)
* feat(gateway): add descriptor-driven interceptors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(gateway): add service-reflected interceptors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * wip * fix(gateway): harden interceptor evaluation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(interceptors): label metrics and harden governance smoke Signed-off-by: Drew Newberry <anewberry@nvidia.com> * remove on_error: ignore * feat(gateway-interceptors): emit log annotations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(examples): govern provider profiles in interceptor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway): preserve update config annotations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(providers): support interceptor profile catalogs Signed-off-by: Drew Newberry <anewberry@nvidia.com> * wip * feat(governance-interceptor): sign provider profiles Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(providers): use configured profile sources for refresh updates Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(gateway-interceptors): add phase-specific evaluation payloads Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(providers): compose provider profile sources Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway-interceptors): preserve committed responses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway-interceptors): reject ambiguous protobuf oneofs Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway-interceptors): validate patch candidates per binding Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(gateway-interceptors): use reflected protobuf codec Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(governance-example): canonicalize signed protobuf hashes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway): commit policy provenance atomically Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway): close signed governance bypasses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway): isolate interceptor secrets and authority Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway): snapshot provider profiles per request Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(gateway): resolve server clippy warnings Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): satisfy provider source clippy lint Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): initialize policy test annotations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(gateway-interceptors): require explicit route allowlist Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(proto): clarify update annotation semantics Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 994750e commit 83003e8

70 files changed

Lines changed: 14420 additions & 570 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# Build output
66
/target/
77
e2e/rust/target/
8+
target/
89
debug/
910
release/
1011

‎Cargo.lock‎

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tonic-prost = "0.14"
2222
tonic-prost-build = "0.14"
2323
prost = "0.14"
2424
prost-types = "0.14"
25+
prost-reflect = { version = "0.16.5", features = ["serde"] }
2526

2627
# HTTP server
2728
axum = { version = "0.8", features = ["ws"] }

‎architecture/gateway.md‎

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,92 @@ Operators can configure a gateway-wide gRPC request rate limit. The limit is
4141
applied only to gRPC API traffic after protocol multiplexing; health, metrics,
4242
and local sandbox-service HTTP routes are not rate limited by this control.
4343

44+
Gateway interceptors run in one middleware layer on the `openshell.v1.OpenShell`
45+
gRPC service after authentication and before tonic dispatches to individual
46+
handlers. At startup the gateway calls each configured interceptor's `Describe`
47+
RPC, validates declared bindings against the compiled OpenShell descriptor set,
48+
and builds an immutable execution plan. Only unary OpenShell methods in the
49+
gateway's explicit interceptable-method allowlist are decoded through the
50+
descriptor set into protobuf JSON, evaluated through configured phases, and
51+
re-encoded before the handler sees the request. New RPCs are non-interceptable
52+
until deliberately added to this allowlist. Interception remains centralized:
53+
allowlisting a unary RPC does not require method-specific gateway
54+
instrumentation.
55+
56+
Each configured interceptor selects a binding policy. `dynamic` accepts valid
57+
manifest declarations and preserves the compatibility behavior. `allowlist`
58+
enables only operator-configured RPCs and phases, while `exact` requires the
59+
configured and declared sets to match. Strict policies match by RPC rather than
60+
manifest binding ID, so renaming a binding does not change authority. Provider
61+
profile sources remain a separate operator-controlled capability.
62+
63+
The protobuf schema marks dedicated credential, token, and refresh-material
64+
fields with a custom secret option. The middleware recursively omits those
65+
fields from every request and post-commit response sent to an interceptor while
66+
retaining the complete protobuf operation for handler dispatch. JSON Patch
67+
paths and source paths cannot select an omitted field or replace a containing
68+
object. There is no configuration that exposes annotated fields.
69+
70+
`SubmitPolicyAnalysis` is interceptable because proposed chunks can eventually
71+
change active policy through the gateway's approval workflow. An interceptor
72+
may therefore reject policy proposals while permitting telemetry-only requests.
73+
Gateways without a matching binding retain the standard proposal behavior.
74+
75+
The descriptor codec uses protobuf's standard `oneof` semantics. If binary
76+
input contains multiple alternatives from one group, the last member on the
77+
wire wins. The middleware converts that selected value to ProtoJSON and
78+
re-encodes it before dispatch, so the interceptor and handler observe the same
79+
canonical request. ProtoJSON input that names multiple alternatives remains
80+
invalid.
81+
82+
Modification results are atomic per binding. After applying one binding's full
83+
JSON Patch list, the middleware re-encodes the candidate as the request's
84+
protobuf type and decodes those accepted bytes back to canonical ProtoJSON.
85+
Invalid candidates follow that binding's failure policy: fail-open restores the
86+
exact pre-binding operation, while fail-closed rejects the request before
87+
handler dispatch. Later bindings only observe the same schema-valid operation
88+
that the handler will receive; protobuf map entry ordering is not treated as a
89+
semantic difference.
90+
91+
Each interceptor evaluation selects exactly one phase payload:
92+
`modify_operation`, `validate`, or `post_commit`. Modification and validation
93+
payloads carry the protobuf JSON operation entering that phase. Post-commit
94+
payloads carry the successful committed response instead of echoing the
95+
request. Only the `validate` payload can also carry optional read-only
96+
`current_state`; modification and post-commit evaluations never receive it. The
97+
gateway does not yet load method-specific state, so the field remains absent;
98+
an absent state is distinct from an explicitly empty object. Method-specific
99+
state schemas and persistence-version binding are deferred until a concrete
100+
consumer requires them.
101+
102+
Post-commit evaluation is strictly observational. A binding that includes
103+
`post_commit` must resolve to `fail_open`, or interceptor initialization fails.
104+
After a handler returns success, failures never replace the committed response.
105+
Binding failures emit the standard fail-open warning and counter; response
106+
observation or evaluation failures outside binding policy emit warnings and the
107+
`openshell_gateway_interceptor_post_commit_observation_failures_total` metric.
108+
The gateway reconstructs the original response frames, including trailers and
109+
body errors, before evaluating the observer.
110+
111+
Interceptor manifests can also vend provider profile catalogs. Gateway
112+
configuration selects the exact ordered source set from the in-tree built-in
113+
source, the stored user source, and named profile-capable interceptors. Omitting
114+
the setting selects `builtin + user`; selecting only an interceptor makes it
115+
authoritative by omission. Every selected source uses the same snapshot,
116+
semantic-validation, and duplicate-detection path. Duplicate normalized profile
117+
IDs fail instead of creating source precedence. The gateway treats configured
118+
interceptors as trusted sources and does not verify signature annotations in
119+
their profile payloads.
120+
121+
Each logical gateway request captures the selected sources into one validated,
122+
immutable effective catalog before deriving provider behavior. Policy layers,
123+
credential scope, injected environment material, dynamic token grants, and
124+
provider-environment revisions use that same catalog. Each configured source is
125+
therefore fetched at most once per request, and a source revision change becomes
126+
visible on the next request instead of partway through the current request. The
127+
capture emits debug diagnostics with the combined catalog revision, source fetch
128+
count, and profile count; it never logs provider credentials or profile material.
129+
44130
Supported auth modes:
45131

46132
| Mode | Use |
@@ -143,6 +229,15 @@ populate `scope`, `version`, `status`, `dedup_key`, and `hit_count` so the
143229
gateway can efficiently fetch the latest policy, track load status, and manage
144230
advisor drafts without creating resource-specific tables.
145231

232+
Each sandbox policy revision stores the complete provenance annotation map
233+
supplied with that update. The revision payload is the authoritative immutable
234+
record; sandbox metadata receives the same annotations only as a convenience
235+
projection and can retain keys from earlier revisions. Policy revision creation,
236+
optional first-policy backfill, metadata projection, and superseding older
237+
revisions commit in one database transaction. SQLite serializes this operation
238+
with an immediate transaction, while Postgres locks the sandbox row. A failed
239+
resource-version check or revision insert rolls back the entire operation.
240+
146241
SQLite is the default local store; Postgres is supported for deployments that
147242
need an external database or multi-replica coordination. Both backends expose
148243
the same `Store` API and the same logical schema. Backend differences stay
@@ -220,7 +315,7 @@ modes:
220315
write. Client-facing operations that carry an `expected_resource_version`
221316
field use this mode: `AttachSandboxProvider`, `DetachSandboxProvider`,
222317
`UpdateProvider`, `UpdateProviderProfiles`, and `UpdateConfig` (policy
223-
backfill path).
318+
backfill and sandbox annotation updates).
224319

225320
**Lists.** The `list_messages` and `list_messages_with_selector` helpers decode
226321
protobuf payloads from list results and hydrate `resource_version` from the

0 commit comments

Comments
 (0)