Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ impl OpenShell for TestOpenShell {
))
}

async fn mint_sandbox_provider_token(
&self,
_request: tonic::Request<openshell_core::proto::MintSandboxProviderTokenRequest>,
) -> Result<Response<openshell_core::proto::MintSandboxProviderTokenResponse>, Status> {
Ok(Response::new(
openshell_core::proto::MintSandboxProviderTokenResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
9 changes: 9 additions & 0 deletions crates/openshell-cli/tests/mtls_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ impl OpenShell for TestOpenShell {
))
}

async fn mint_sandbox_provider_token(
&self,
_request: tonic::Request<openshell_core::proto::MintSandboxProviderTokenRequest>,
) -> Result<Response<openshell_core::proto::MintSandboxProviderTokenResponse>, Status> {
Ok(Response::new(
openshell_core::proto::MintSandboxProviderTokenResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
9 changes: 9 additions & 0 deletions crates/openshell-cli/tests/provider_commands_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ impl OpenShell for TestOpenShell {
))
}

async fn mint_sandbox_provider_token(
&self,
_request: tonic::Request<openshell_core::proto::MintSandboxProviderTokenRequest>,
) -> Result<Response<openshell_core::proto::MintSandboxProviderTokenResponse>, Status> {
Ok(Response::new(
openshell_core::proto::MintSandboxProviderTokenResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ impl OpenShell for TestOpenShell {
))
}

async fn mint_sandbox_provider_token(
&self,
_request: tonic::Request<openshell_core::proto::MintSandboxProviderTokenRequest>,
) -> Result<Response<openshell_core::proto::MintSandboxProviderTokenResponse>, Status> {
Ok(Response::new(
openshell_core::proto::MintSandboxProviderTokenResponse::default(),
))
}

async fn create_ssh_session(
&self,
request: tonic::Request<CreateSshSessionRequest>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ impl OpenShell for TestOpenShell {
))
}

async fn mint_sandbox_provider_token(
&self,
_request: tonic::Request<openshell_core::proto::MintSandboxProviderTokenRequest>,
) -> Result<Response<openshell_core::proto::MintSandboxProviderTokenResponse>, Status> {
Ok(Response::new(
openshell_core::proto::MintSandboxProviderTokenResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
6 changes: 6 additions & 0 deletions crates/openshell-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ impl ProviderRegistry {
registry.register(providers::nvidia::NvidiaProvider);
registry.register(providers::gitlab::GitlabProvider);
registry.register(providers::github::GithubProvider);
registry.register(providers::microsoft_agent_s2s::MicrosoftAgentS2sProvider);
registry.register(providers::outlook::OutlookProvider);
registry
}
Expand Down Expand Up @@ -153,6 +154,7 @@ pub fn normalize_provider_type(input: &str) -> Option<&'static str> {
"nvidia" => Some("nvidia"),
"gitlab" | "glab" => Some("gitlab"),
"github" | "gh" => Some("github"),
"microsoft-agent-s2s" => Some("microsoft-agent-s2s"),
"outlook" => Some("outlook"),
_ => None,
}
Expand Down Expand Up @@ -183,6 +185,10 @@ mod tests {
assert_eq!(normalize_provider_type("anthropic"), Some("anthropic"));
assert_eq!(normalize_provider_type("nvidia"), Some("nvidia"));
assert_eq!(normalize_provider_type("copilot"), Some("copilot"));
assert_eq!(
normalize_provider_type("microsoft-agent-s2s"),
Some("microsoft-agent-s2s")
);
assert_eq!(normalize_provider_type("unknown"), None);
}

Expand Down
18 changes: 18 additions & 0 deletions crates/openshell-providers/src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const BUILT_IN_PROFILE_YAMLS: &[&str] = &[
include_str!("../../../providers/copilot.yaml"),
include_str!("../../../providers/github.yaml"),
include_str!("../../../providers/gitlab.yaml"),
include_str!("../../../providers/microsoft-agent-s2s.yaml"),
include_str!("../../../providers/nvidia.yaml"),
include_str!("../../../providers/openai.yaml"),
include_str!("../../../providers/opencode.yaml"),
Expand Down Expand Up @@ -883,6 +884,23 @@ mod tests {
assert_eq!(proto.binaries.len(), 4);
}

#[test]
fn microsoft_agent_s2s_profile_is_available() {
let profile =
get_default_profile("microsoft-agent-s2s").expect("microsoft-agent-s2s profile");
let proto = profile.to_proto();

assert_eq!(proto.id, "microsoft-agent-s2s");
assert_eq!(proto.category, ProviderProfileCategory::Agent as i32);
assert_eq!(proto.credentials.len(), 1);
assert_eq!(proto.credentials[0].name, "blueprint_client_secret");
assert_eq!(
proto.credentials[0].env_vars,
vec!["A365_BLUEPRINT_CLIENT_SECRET"]
);
assert!(proto.endpoints.is_empty());
}

#[test]
fn credential_env_vars_are_deduplicated_in_profile_order() {
let profile = get_default_profile("copilot").expect("copilot profile");
Expand Down
111 changes: 111 additions & 0 deletions crates/openshell-providers/src/providers/microsoft_agent_s2s.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES.
// SPDX-License-Identifier: Apache-2.0

use crate::DiscoveryContext;
use crate::{DiscoveredProvider, ProviderError, ProviderPlugin, RealDiscoveryContext};

pub struct MicrosoftAgentS2sProvider;

const CREDENTIAL_ENV_VARS: &[&str] = &["A365_BLUEPRINT_CLIENT_SECRET"];
const CONFIG_ENV_VARS: &[&str] = &[
"AZURE_TENANT_ID",
"A365_BLUEPRINT_CLIENT_ID",
"A365_RUNTIME_AGENT_ID",
"A365_ALLOWED_AUDIENCES",
"A365_OBSERVABILITY_RESOURCE",
"A365_REQUIRED_ROLES",
];

impl ProviderPlugin for MicrosoftAgentS2sProvider {
fn id(&self) -> &'static str {
"microsoft-agent-s2s"
}

fn discover_existing(&self) -> Result<Option<DiscoveredProvider>, ProviderError> {
discover_microsoft_agent_s2s(&RealDiscoveryContext)
}

fn credential_env_vars(&self) -> &'static [&'static str] {
CREDENTIAL_ENV_VARS
}
}

fn discover_microsoft_agent_s2s(
context: &dyn DiscoveryContext,
) -> Result<Option<DiscoveredProvider>, ProviderError> {
let mut discovered = DiscoveredProvider::default();

for key in CREDENTIAL_ENV_VARS {
if let Some(value) = context.env_var(key)
&& !value.trim().is_empty()
{
discovered
.credentials
.entry((*key).to_string())
.or_insert(value);
}
}

for key in CONFIG_ENV_VARS {
if let Some(value) = context.env_var(key)
&& !value.trim().is_empty()
{
discovered.config.entry((*key).to_string()).or_insert(value);
}
}

if discovered.is_empty() {
Ok(None)
} else {
Ok(Some(discovered))
}
}

#[cfg(test)]
mod tests {
use super::discover_microsoft_agent_s2s;
use crate::test_helpers::MockDiscoveryContext;

#[test]
fn discovers_microsoft_agent_s2s_env_state() {
let ctx = MockDiscoveryContext::new()
.with_env("AZURE_TENANT_ID", "tenant-id")
.with_env("A365_BLUEPRINT_CLIENT_ID", "blueprint-client-id")
.with_env("A365_BLUEPRINT_CLIENT_SECRET", "blueprint-secret")
.with_env("A365_RUNTIME_AGENT_ID", "runtime-agent-id")
.with_env("A365_ALLOWED_AUDIENCES", "api://aud-a,api://aud-b")
.with_env("A365_OBSERVABILITY_RESOURCE", "observability-resource")
.with_env("A365_REQUIRED_ROLES", "Agent365.Observability.OtelWrite");
let discovered = discover_microsoft_agent_s2s(&ctx)
.expect("discovery")
.expect("provider");
assert_eq!(
discovered.credentials.get("A365_BLUEPRINT_CLIENT_SECRET"),
Some(&"blueprint-secret".to_string())
);
assert_eq!(
discovered.config.get("AZURE_TENANT_ID"),
Some(&"tenant-id".to_string())
);
assert_eq!(
discovered.config.get("A365_BLUEPRINT_CLIENT_ID"),
Some(&"blueprint-client-id".to_string())
);
assert_eq!(
discovered.config.get("A365_RUNTIME_AGENT_ID"),
Some(&"runtime-agent-id".to_string())
);
assert_eq!(
discovered.config.get("A365_ALLOWED_AUDIENCES"),
Some(&"api://aud-a,api://aud-b".to_string())
);
assert_eq!(
discovered.config.get("A365_OBSERVABILITY_RESOURCE"),
Some(&"observability-resource".to_string())
);
assert_eq!(
discovered.config.get("A365_REQUIRED_ROLES"),
Some(&"Agent365.Observability.OtelWrite".to_string())
);
}
}
1 change: 1 addition & 0 deletions crates/openshell-providers/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod copilot;
pub mod generic;
pub mod github;
pub mod gitlab;
pub mod microsoft_agent_s2s;
pub mod nvidia;
pub mod openai;
pub mod opencode;
Expand Down
43 changes: 42 additions & 1 deletion crates/openshell-sandbox/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use std::time::Duration;
use miette::{IntoDiagnostic, Result, WrapErr};
use openshell_core::proto::{
DenialSummary, GetDraftPolicyRequest, GetInferenceBundleRequest, GetInferenceBundleResponse,
GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, PolicyChunk, PolicySource,
GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, MintSandboxProviderTokenRequest,
PolicyChunk, PolicySource,
PolicyStatus, ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy,
SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UpdateConfigRequest,
inference_client::InferenceClient, open_shell_client::OpenShellClient,
Expand Down Expand Up @@ -230,6 +231,39 @@ pub async fn fetch_provider_environment(
})
}

pub async fn mint_provider_token(
endpoint: &str,
sandbox_id: &str,
provider_name: &str,
audience: &str,
) -> Result<MintedProviderToken> {
debug!(
endpoint = %endpoint,
sandbox_id = %sandbox_id,
provider_name = %provider_name,
audience = %audience,
"Minting sandbox provider token"
);

let mut client = connect(endpoint).await?;
let response = client
.mint_sandbox_provider_token(MintSandboxProviderTokenRequest {
sandbox_id: sandbox_id.to_string(),
provider_name: provider_name.to_string(),
audience: audience.to_string(),
})
.await
.into_diagnostic()?;

let inner = response.into_inner();
Ok(MintedProviderToken {
access_token: inner.access_token,
token_type: inner.token_type,
expires_at_unix: inner.expires_at_unix,
cache_hit: inner.cache_hit,
})
}

/// A reusable gRPC client for the `OpenShell` service.
///
/// Wraps a tonic channel connected once and reused for policy polling
Expand Down Expand Up @@ -258,6 +292,13 @@ pub struct ProviderEnvironmentResult {
pub provider_env_revision: u64,
}

pub struct MintedProviderToken {
pub access_token: String,
pub token_type: String,
pub expires_at_unix: u64,
pub cache_hit: bool,
}

impl CachedOpenShellClient {
pub async fn connect(endpoint: &str) -> Result<Self> {
debug!(endpoint = %endpoint, "Connecting openshell gRPC client for policy polling");
Expand Down
Loading
Loading