Skip to content

Commit ba19ea3

Browse files
committed
refactor(supervisor): prepare desired-state snapshots
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 0618ab0 commit ba19ea3

4 files changed

Lines changed: 317 additions & 74 deletions

File tree

crates/openshell-sandbox/src/lib.rs

Lines changed: 89 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3948,47 +3948,12 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
39483948
{
39493949
Ok(env_result) => {
39503950
let provider_env_revision = env_result.provider_env_revision;
3951-
let install_result = ctx.provider_credentials.install_bound_environment(
3952-
provider_env_revision,
3953-
env_result.environment,
3954-
env_result.credential_expires_at_ms,
3955-
env_result.dynamic_credentials,
3956-
env_result.static_credential_bindings,
3957-
env_result.non_secret_environment_keys,
3958-
);
3959-
if let Err(error) = install_result {
3960-
ocsf_emit!(
3961-
ConfigStateChangeBuilder::new(ocsf_ctx())
3962-
.severity(SeverityId::High)
3963-
.status(StatusId::Failure)
3964-
.state(StateId::Disabled, "fail_closed")
3965-
.message(format!(
3966-
"Rejected provider environment refresh; static provider credentials were revoked; fetched dynamic token grants remain active: {error}"
3967-
))
3968-
.build()
3969-
);
3970-
} else {
3971-
let child_env = ctx.provider_credentials.child_env_with_gcp_resolved();
3972-
let env_count = child_env.len();
3973-
if let Some(publisher) = ctx.sidecar_control_publisher.as_ref() {
3974-
publisher
3975-
.publish_provider_env(provider_env_revision, child_env.clone());
3976-
}
3951+
if apply_provider_environment_snapshot(
3952+
&ctx.provider_credentials,
3953+
env_result,
3954+
ctx.sidecar_control_publisher.as_ref(),
3955+
) {
39773956
current_provider_env_revision = provider_env_revision;
3978-
ocsf_emit!(
3979-
ConfigStateChangeBuilder::new(ocsf_ctx())
3980-
.severity(SeverityId::Informational)
3981-
.status(StatusId::Success)
3982-
.state(StateId::Enabled, "loaded")
3983-
.unmapped(
3984-
"provider_env_revision",
3985-
serde_json::json!(provider_env_revision)
3986-
)
3987-
.message(format!(
3988-
"Provider environment refreshed [revision:{provider_env_revision} env_count:{env_count}]"
3989-
))
3990-
.build()
3991-
);
39923957
}
39933958
}
39943959
Err(e) => {
@@ -4262,6 +4227,61 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
42624227
}
42634228
}
42644229

4230+
/// Apply one complete provider-environment snapshot to the live credential state.
4231+
///
4232+
/// The caller remains responsible for serializing snapshots and deciding whether
4233+
/// a failed application should be retried. Keeping transport outside this helper
4234+
/// lets polling and supervisor-session updates share the same installation path.
4235+
fn apply_provider_environment_snapshot(
4236+
provider_credentials: &ProviderCredentialState,
4237+
snapshot: openshell_core::grpc_client::ProviderEnvironmentResult,
4238+
sidecar_control_publisher: Option<&sidecar_control::Publisher>,
4239+
) -> bool {
4240+
let provider_env_revision = snapshot.provider_env_revision;
4241+
let install_result = provider_credentials.install_bound_environment(
4242+
provider_env_revision,
4243+
snapshot.environment,
4244+
snapshot.credential_expires_at_ms,
4245+
snapshot.dynamic_credentials,
4246+
snapshot.static_credential_bindings,
4247+
snapshot.non_secret_environment_keys,
4248+
);
4249+
if let Err(error) = install_result {
4250+
ocsf_emit!(
4251+
ConfigStateChangeBuilder::new(ocsf_ctx())
4252+
.severity(SeverityId::High)
4253+
.status(StatusId::Failure)
4254+
.state(StateId::Disabled, "fail_closed")
4255+
.message(format!(
4256+
"Rejected provider environment refresh; static provider credentials were revoked; fetched dynamic token grants remain active: {error}"
4257+
))
4258+
.build()
4259+
);
4260+
return false;
4261+
}
4262+
4263+
let child_env = provider_credentials.child_env_with_gcp_resolved();
4264+
let env_count = child_env.len();
4265+
if let Some(publisher) = sidecar_control_publisher {
4266+
publisher.publish_provider_env(provider_env_revision, child_env);
4267+
}
4268+
ocsf_emit!(
4269+
ConfigStateChangeBuilder::new(ocsf_ctx())
4270+
.severity(SeverityId::Informational)
4271+
.status(StatusId::Success)
4272+
.state(StateId::Enabled, "loaded")
4273+
.unmapped(
4274+
"provider_env_revision",
4275+
serde_json::json!(provider_env_revision)
4276+
)
4277+
.message(format!(
4278+
"Provider environment refreshed [revision:{provider_env_revision} env_count:{env_count}]"
4279+
))
4280+
.build()
4281+
);
4282+
true
4283+
}
4284+
42654285
fn apply_ocsf_json_setting(
42664286
enabled: &AtomicBool,
42674287
settings: &std::collections::HashMap<String, openshell_core::proto::EffectiveSetting>,
@@ -4498,6 +4518,35 @@ mod tests {
44984518
);
44994519
}
45004520

4521+
#[test]
4522+
fn provider_environment_snapshot_apply_installs_complete_snapshot() {
4523+
let provider_credentials =
4524+
ProviderCredentialState::from_child_env_snapshot(1, std::collections::HashMap::new());
4525+
let applied = apply_provider_environment_snapshot(
4526+
&provider_credentials,
4527+
openshell_core::grpc_client::ProviderEnvironmentResult {
4528+
environment: std::collections::HashMap::from([(
4529+
"API_BASE".to_string(),
4530+
"https://example.test".to_string(),
4531+
)]),
4532+
provider_env_revision: 7,
4533+
credential_expires_at_ms: std::collections::HashMap::new(),
4534+
dynamic_credentials: std::collections::HashMap::new(),
4535+
static_credential_bindings: std::collections::HashMap::new(),
4536+
non_secret_environment_keys: vec!["API_BASE".to_string()],
4537+
},
4538+
None,
4539+
);
4540+
4541+
assert!(applied);
4542+
let snapshot = provider_credentials.snapshot();
4543+
assert_eq!(snapshot.revision, 7);
4544+
assert_eq!(
4545+
snapshot.child_env.get("API_BASE").map(String::as_str),
4546+
Some("openshell:resolve:env:v7_API_BASE")
4547+
);
4548+
}
4549+
45014550
#[tokio::test]
45024551
async fn sidecar_control_provider_env_update_orders_by_generation() {
45034552
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

crates/openshell-server/src/grpc/policy.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,6 +2321,21 @@ pub(super) async fn handle_get_sandbox_config(
23212321

23222322
let sandbox =
23232323
super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?;
2324+
build_sandbox_config_snapshot(state, &sandbox)
2325+
.await
2326+
.map(Response::new)
2327+
}
2328+
2329+
/// Build the effective gateway-owned configuration for an authorized sandbox.
2330+
///
2331+
/// Keeping request authentication outside this function lets supervisor session
2332+
/// bootstrap and reconciliation reuse the same snapshot construction path as
2333+
/// the public `GetSandboxConfig` RPC.
2334+
pub async fn build_sandbox_config_snapshot(
2335+
state: &ServerState,
2336+
sandbox: &Sandbox,
2337+
) -> Result<GetSandboxConfigResponse, Status> {
2338+
let sandbox_id = sandbox.object_id().to_string();
23242339
let workspace = sandbox.object_workspace().to_string();
23252340
let sandbox_provider_names = sandbox
23262341
.spec
@@ -2498,7 +2513,7 @@ pub(super) async fn handle_get_sandbox_config(
24982513
);
24992514
if let Some(policy) = policy.as_ref() {
25002515
validate_policy_credential_bindings_for_sandbox(
2501-
state.as_ref(),
2516+
state,
25022517
&provider_profile_catalog,
25032518
&workspace,
25042519
&sandbox_provider_names,
@@ -2515,7 +2530,7 @@ pub(super) async fn handle_get_sandbox_config(
25152530
)
25162531
.await?;
25172532

2518-
Ok(Response::new(GetSandboxConfigResponse {
2533+
Ok(GetSandboxConfigResponse {
25192534
policy,
25202535
version,
25212536
policy_hash,
@@ -2532,7 +2547,7 @@ pub(super) async fn handle_get_sandbox_config(
25322547
.as_str()
25332548
.to_string(),
25342549
extension_authentication_enabled: state.sandbox_jwt_issuer.is_some(),
2535-
}))
2550+
})
25362551
}
25372552

25382553
#[cfg(test)]
@@ -3042,6 +3057,23 @@ pub(super) async fn handle_get_sandbox_provider_environment(
30423057
.await
30433058
.map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))?
30443059
.ok_or_else(|| Status::not_found("sandbox not found"))?;
3060+
3061+
build_provider_environment_snapshot(state, &sandbox, supports_static_credential_bindings)
3062+
.await
3063+
.map(Response::new)
3064+
}
3065+
3066+
/// Build the gateway-owned provider environment for an authorized sandbox.
3067+
///
3068+
/// `supports_static_credential_bindings` preserves the existing fetch RPC's
3069+
/// compatibility behavior. The required session protocol introduced by
3070+
/// #1731 will call this builder with binding support enabled.
3071+
pub async fn build_provider_environment_snapshot(
3072+
state: &ServerState,
3073+
sandbox: &Sandbox,
3074+
supports_static_credential_bindings: bool,
3075+
) -> Result<GetSandboxProviderEnvironmentResponse, Status> {
3076+
let sandbox_id = sandbox.object_id().to_string();
30453077
let workspace = sandbox.object_workspace().to_string();
30463078

30473079
let spec = sandbox
@@ -3061,10 +3093,10 @@ pub(super) async fn handle_get_sandbox_provider_environment(
30613093
)
30623094
.await?;
30633095
let effective_policy = current_effective_policy_for_sandbox(
3064-
state.as_ref(),
3096+
state,
30653097
&provider_profile_catalog,
30663098
&workspace,
3067-
&sandbox,
3099+
sandbox,
30683100
&sandbox_id,
30693101
)
30703102
.await?;
@@ -3136,14 +3168,14 @@ pub(super) async fn handle_get_sandbox_provider_environment(
31363168
.cloned()
31373169
.collect();
31383170

3139-
Ok(Response::new(GetSandboxProviderEnvironmentResponse {
3171+
Ok(GetSandboxProviderEnvironmentResponse {
31403172
environment: provider_environment.environment,
31413173
provider_env_revision,
31423174
credential_expires_at_ms: provider_environment.credential_expires_at_ms,
31433175
dynamic_credentials: provider_environment.dynamic_credentials,
31443176
static_credential_bindings: provider_environment.static_credential_bindings,
31453177
non_secret_environment_keys,
3146-
}))
3178+
})
31473179
}
31483180

31493181
// ---------------------------------------------------------------------------
@@ -9673,6 +9705,54 @@ mod tests {
96739705
);
96749706
}
96759707

9708+
#[tokio::test]
9709+
async fn snapshot_builders_match_existing_fetch_rpc_payloads() {
9710+
use openshell_core::proto::GetSandboxProviderEnvironmentRequest;
9711+
9712+
let state = test_server_state().await;
9713+
state
9714+
.store
9715+
.put_message(&test_provider("work-github", "github"))
9716+
.await
9717+
.unwrap();
9718+
let sandbox = test_sandbox(
9719+
"sb-builder-parity",
9720+
"builder-parity",
9721+
test_policy_with_rule("sandbox_only", "sandbox.example.com"),
9722+
vec!["work-github".to_string()],
9723+
);
9724+
state.store.put_message(&sandbox).await.unwrap();
9725+
9726+
let built_config = build_sandbox_config_snapshot(&state, &sandbox)
9727+
.await
9728+
.unwrap();
9729+
let fetched_config = handle_get_sandbox_config(
9730+
&state,
9731+
with_user(Request::new(GetSandboxConfigRequest {
9732+
sandbox_id: sandbox.object_id().to_string(),
9733+
})),
9734+
)
9735+
.await
9736+
.unwrap()
9737+
.into_inner();
9738+
assert_eq!(built_config, fetched_config);
9739+
9740+
let built_environment = build_provider_environment_snapshot(&state, &sandbox, true)
9741+
.await
9742+
.unwrap();
9743+
let fetched_environment = handle_get_sandbox_provider_environment(
9744+
&state,
9745+
with_user(Request::new(GetSandboxProviderEnvironmentRequest {
9746+
sandbox_id: sandbox.object_id().to_string(),
9747+
supports_static_credential_bindings: true,
9748+
})),
9749+
)
9750+
.await
9751+
.unwrap()
9752+
.into_inner();
9753+
assert_eq!(built_environment, fetched_environment);
9754+
}
9755+
96769756
#[tokio::test]
96779757
async fn provider_environment_resolution_is_unchanged_by_providers_v2_setting() {
96789758
use openshell_core::proto::GetSandboxProviderEnvironmentRequest;

0 commit comments

Comments
 (0)