Repository navigation
bug(gateway): provider environment revision is not deterministic when a provider profile has several annotations #3929
Description
Activity
- addedstate:triage-neededOpened without agent diagnostics and needs triageOpened without agent diagnostics and needs triage
on Sep 30, 2026 📋 triage-agent
Triage Assessment
Classification: validated-bug
Summary
Confirmed on
mainand inv0.1.0, the reporter's version, with high confidence.openshell-coregenerates protobuf map fields asstd::collections::HashMapbecausecrates/openshell-core/build.rsconfigures nobtree_map. prost encodes map entries in iteration order. Every request re-decodes provider profiles from the store, so each decode builds a newHashMapwith a freshRandomState. The profile'sencode_to_vec()output that feeds the provider environment revision therefore varies between calls whenever a map field has two or more entries.Investigation
- Empirical reproduction on
main. AProviderProfilewith two annotations, encoded once, then decoded and re-encoded 200 times, produced exactly 2 distinct byte encodings. This matches the reported alternation between twoprovider_env_revisionvalues. With n entries, up to n! orderings are possible. - Hash sites that include profile encodings:
crates/openshell-server/src/provider_profile_sources.rs:105and:126(user profile source revision)provider_profile_sources.rs:498,hash_scoped_profile_revision. Reached fromgrpc/policy.rsthroughcompute_provider_env_revision_with_catalog_and_policy_bindingsandcompute_provider_env_revision_from_records_and_policy_bindings.provider_profile_sources.rs:712is test-only.
- Already deterministic, not affected:
- The provider's own
credentialsandcredential_expiration_times, whichpolicy.rskey-sorts before hashing. - The policy part of the revision, which uses
deterministic_policy_hashinopenshell-core/src/policy_identity.rs. - Only the profile encoding path lacks canonicalization.
- The provider's own
- Consistency within a call. Order is stable inside a single decoded instance, because
HashMap::clonepreserves hasher keys and layout. Each RPC builds its own catalog throughsnapshot_catalog, though, soGetSandboxConfigandGetSandboxProviderEnvironmentindependently land on one of the orderings and disagree about half the time. Separate gateway replicas would also disagree. - Supervisor reaction, which explains the logs:
openshell-supervisor/src/lib.rssetsprovider_env_changed, requires the fetched environment identity to equal the desired identity, and otherwise emits theFAIL_CLOSED"static credentials were revoked" event and revokes the static environment.- Startup gives up after 5 unstable attempts.
- On the server,
configuration_generation_matchesingrpc/policy.rsproduces theReportEndpointStatusFailedPrecondition.
- Map fields affected beyond
annotations.ProviderProfile.endpoints(sandbox.v1.NetworkEndpoint) carries nested maps:graphql_persisted_queries,L7Allow.query/paramsandL7DenyRule.query/params. Whether profile validation lets endpoints carry them was not checked; it affects scope, not validity. Profiles vended by interceptor sources go through the same:498path. - Existing tests. No test uses a profile with more than one map entry, so the gap is untested.
- Duplicates. None found. bug(supervisor): accept distinct provider environment revisions in process sidecar #2847 (closed) also involved
provider_env_revision, but had a different cause: the process watcher treated the digest as a monotonic counter.
Impact Signals
- Affected users/scope: Any sandbox bound to a provider whose profile has two or more entries in any map field (annotations today, possibly nested endpoint maps). Static provider credentials are revoked on roughly half of settings polls, endpoint status reports fail, and startup can fail to stabilize. It fails closed: credentials are withheld, not exposed.
- Regression: Unknown. The hashing is present in
v0.1.0and onmain; no earlier known-good version was identified. - Workaround: Available but limiting: keep at most one entry in every map field of provider profiles.
- Evidence quality: High. The code path was traced and the nondeterministic encoding was reproduced with a unit-level test.
Candidate fix directions for whoever plans the work, not a plan: configure
btree_mapfor the affected map paths inopenshell-core/build.rs, which changes generated Rust types across crates and the Rust SDK, or canonicalize profile maps before hashing, reusing the sorted-map approach inpolicy_identity.rs. With either, a regression test with several annotations should assert that revisions are equal across decodes. Revision values will change once on upgrade, which triggers one benign provider-env refresh.Human Decision Required
Decide whether OpenShell should address this issue. If yes, apply
state:accepted, associate it with a roadmap item, or do both, and decide
whether the work remains human-owned. Either action records acceptance;
roadmap placement additionally records sequencing.
To queue investigation or planning for an unattended agent, also apply
agent:plan-requested. You can instead directly ask an agent to use
create-spikeorbuild-from-issueon this issue; the agent will warn about
missing expected workflow labels and continue without changing them. If no,
close it as not planned and record the rationale.Suggested labels (not applied, because the triaging account lacks triage permission):
area:gateway,area:providers; replacestate:triage-neededwithstate:validated.- Empirical reproduction on
hey @rain-sicoreai, I'd like to take this one
- added a commit that references this issue
on Sep 30, 2026
User Story
As a platform operator driving the OpenShell gateway API (operator workspace mode, Kubernetes driver) to create sandboxes with bound provider credentials,
I want the provider environment revision to be stable while nothing changes,
so that the supervisor installs the provider credentials and policy updates apply promptly.
Problem Statement
When a provider profile has more than one entry in a map field (for example two
annotations), the gateway reports a different provider environment revision on successive calls although no provider, profile or policy changed. In our runsGetSandboxProviderEnvironmentalternated between exactly twoprovider_env_revisionvalues from poll to poll for the same sandbox, and the configuration snapshot's revision did not match the environment's.The revision hash includes the profile's protobuf encoding (
hash_scoped_profile_revisionincrates/openshell-server/src/provider_profile_sources.rscallsentry.response.encode_to_vec(); also lines 105 and 126 onmain).ProviderProfile.annotationsismap<string, string>andopenshell-core's build does not configure ordered maps, so the encoded bytes depend on hash-map iteration order.Impact / Why This Matters
Settings poll: config change detected [... provider_env_changed:true], thenCONFIG:FAIL_CLOSED [HIGH] Provider environment refresh failed; static credentials were revoked ...andProvider environment is unavailable or changed during preparation: provider credentials are withheld from the workload for as long as the sandbox runs.ReportEndpointStatusfails withFailedPrecondition("tool server endpoint status revisions do not match the current sandbox configuration") on every report.Startup configuration did not stabilize after 5 attempts, and a policy update took ~60 s to load instead of ~10 s, because preparation keeps being retried.Acceptance Criteria
GetSandboxProviderEnvironment) is identical across calls when providers, profiles and policy are unchanged, for profiles with several annotations (and any other map field).FAIL_CLOSEDprovider refresh events and noReportEndpointStatusFailedPreconditionin steady state.Reproduction Steps
ImportProviderProfileswithannotations: {"example.com/a": "1", "example.com/b": "2"}and one credential.credential_binding).GetSandboxProviderEnvironment request completed successfully ... provider_env_revision=<n>over a few minutes: the value alternates between two numbers.provider_env_changed:trueandCONFIG:FAIL_CLOSEDevents.Environment
mainas of 2026-09-30workspace_mode = "operator", driven through the gateway gRPC API by a control plane that creates sandboxes, provider profiles and providersLogs
Gateway, same sandbox, consecutive polls (no changes in between):