Repository navigation
feat: expose current and candidate effective policies to validate-phase gateway interceptors #4793
Description
User Story
I work at Gradient Technologies and I am building an OpenShell integration for our product. Our authorization service would sit behind a gateway interceptor and needs to approve or deny sandbox policy changes before they commit.
To decide, it has to see both the policy that is currently enforced and the one that would be enforced after the change, with provider layers composed. Today a validate-phase interceptor only gets the request, so our service can't see what is enforced now or what will be enforced after the change. The alternative is to move the check after commit, which cannot block anything.
I am happy to implement this.
Problem Statement
A validate-phase interceptor cannot see either effective policy.
Current. ValidateEvaluation.current_state is the field that could carry it, but the gateway always sets
validate_current_state: None. The docs record this under Current Limitations: "current_state is available only in the validate contract. The gateway does not yet populate it with method-specific state."
Candidate. There is no field for it. Provider layers are composed by compose_effective_policy inside the RPC handlers, which run after the interceptor pipeline in the multiplex wrapper.
OpenShell already builds both for human reviewers: current_effective_policy, candidate_effective_policy and both hashes are delivered for draft-chunk review. This asks for the same pair at the interceptor contract.
Impact / Why This Matters
- An interceptor that approves or denies policy changes is checking the wrong thing. For a full policy update, the request has the new base policy without the provider rules. For a merge update, it has only the changes, so there is no complete policy to check.
- Without the current effective policy there is no before-and-after comparison, so the decision cannot be about what the change actually does.
validateis the last phase that can deny and the only one that sees the request after all modifications. An interceptor that cannot see the effective policy there cannot prevent anything.post_commitreceives onlycommitted_response. ForUpdateConfigthat is a version, a base-policy hash, a settings revision and annotations. No policy. Detecting a bad effective policy means a separateGetSandboxConfigread after the change is already live.- Composing outside the gateway means mirroring gateway-owned state: attached providers, the profile catalog, and layer derivation. Any drift produces a wrong decision.
- The same gap affects any out-of-process check against an effective policy, including running
openshell-proverexternally.
Proposed Design
Keep current_state to its defined meaning (state loaded before the operation) and add a field for the candidate:
message ValidateEvaluation {
google.protobuf.Struct proposed_operation = 1;
// Read-only gateway state loaded before applying the operation.
google.protobuf.Struct current_state = 2;
// Effective policy the gateway would apply if this operation commits.
google.protobuf.Struct effective_candidate = 3;
}What an opted-in interceptor would receive:
{
"interceptorName": "authz-service",
"bindingId": "validate-update-config",
"service": "openshell.v1.OpenShell",
"method": "UpdateConfig",
"principal": { "kind": "user", "subject": "..." },
"validate": {
"proposedOperation": { "...": "as today" },
"currentState": {
"effectivePolicy": { "...": "current base composed with provider layers" },
"policyHash": "<hex>"
},
"effectiveCandidate": {
"policy": { "...": "candidate base composed with provider layers" },
"policyHash": "<hex>",
"providerLayers": ["vault-prod", "github-ro"]
}
}
}Opt in per binding, defaulting to off, so interceptors that do not need it pay nothing:
[[openshell.gateway.interceptors]]
name = "authz-service"
grpc_endpoint = "unix:///run/authz.sock"
failure_policy = "fail_closed"
binding_policy = "allowlist"
[[openshell.gateway.interceptors.bindings]]
rpc = "openshell.v1.OpenShell/UpdateConfig"
phases = ["validate"]
include_effective_policy = trueScope: sandbox-scoped UpdateConfig. Global UpdateConfig applies across sandboxes and the effective policy is per sandbox, so one evaluation cannot carry it.
Field names and the config key are illustrative. The requirement is that a validate-phase interceptor can see the current effective policy and the one the commit will produce, and bind its decision to the candidate's hash.
Acceptance Criteria
- A validate-phase interceptor on sandbox-scoped
UpdateConfigreceives the current and candidate effective policy, including composed provider layers. -
effectiveCandidate.policyHashequals theSandboxConfigSnapshot.policy_hashdelivered to the supervisor for the committed revision, for an unchanged candidate. - Bindings that do not opt in see no change in payload or latency.
-
CreateSandbox,AttachSandboxProvider,DetachSandboxProvider,ApproveDraftChunkandApproveAllDraftChunksbehave consistently withUpdateConfig.SubmitPolicyAnalysiswith auto-approval enabled carries a candidate reflecting the auto-approved chunks. - A denial from a validate-phase interceptor prevents the change, with existing failure-policy semantics unchanged.
- The Current Limitations entry in
docs/extensibility/gateway-interceptors.mdxis updated.
Alternatives Considered
- Supervisor middleware. Per request at runtime. Cannot gate a policy change.
post_commitinterceptor. Audit-only, and receives only the RPC response: version and base-policy hash, not the policy. Observing the effective policy requires a separateGetSandboxConfigread after commit.- Link
openshell-policyand compose in the interceptor.compose_effective_policyis public, but the provider layers it takes are built from gateway-owned state: attached provider records, the workspace's profile catalog, endpoint activation and token-grant ownership (provider_policy_context_from_records, private to the server). An interceptor would have to mirror all of it, and any drift produces a wrong decision. - Interceptor as
provider_profile_sourceplus attach/detach bindings. Same objection with more surface: the interceptor tracks attachment state and layer derivation outside the gateway, and the failure mode is silent. - Read back via
GetSandboxConfig. Only available after commit, and racy. - Do nothing. External validation stays advisory.
Agent Investigation
Read at d2239437 (main, 2026-10-21):
- gateway_interceptor.proto L74-75:
current_statedocumented as state loaded before applying the operation; no field carries the candidate. - multiplex.rs L702:
validate_current_state: None. - gateway-interceptors.mdx L171: the limitation stated in the docs.
- openshell.proto L3513-3518: draft-chunk review already carries
current_effective_policy,candidate_effective_policyand both hashes. - multiplex.rs L441:
hydrate_update_provider_identityalready loads gateway state before interceptors run, which is the precedent for composing in the wrapper. - policy_identity.rs L175:
deterministic_policy_hashalready hashes the effective policy forSandboxConfigSnapshotand draft-chunk review. No new hash machinery needed. - policy.rs L4734:
UpdateConfigResponse.policy_hashhashes the candidate base policy, not the candidate effective policy. - lib.rs L37-40: exports
compose_effective_policy. - policy.rs L3528: (
provider_policy_context_from_records) builds its layers and is private to the server.
Checklist
- I've reviewed existing issues and the published docs
- This is a design proposal, not a "please build this" request
Activity