use std::collections::BTreeMap;
use std::convert::Infallible;
use std::future::Future;
use tonic::codegen::async_trait;
use crate::agent_provider::AgentToolRef;
use crate::catalog::Catalog;
use crate::error::{Error, Result};
use crate::proto::v1;
pub fn parse_subject_id(subject_id: &str) -> Option<(&str, &str)> {
let trimmed = subject_id.trim();
let (kind, id) = trimmed.split_once(':')?;
let kind = kind.trim();
let id = id.trim();
if kind.is_empty() || id.is_empty() {
return None;
}
Some((kind, id))
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Subject {
pub id: String,
pub credential_subject_id: String,
pub email: String,
pub display_name: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Credential {
pub mode: String,
pub subject_id: String,
pub connection: String,
pub instance: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Access {
pub policy: String,
pub role: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Host {
pub public_base_url: String,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Request {
pub token: String,
pub connection_params: BTreeMap<String, String>,
pub subject: Subject,
pub agent_subject: Subject,
pub credential: Credential,
pub access: Access,
pub host: Host,
pub idempotency_key: String,
pub workflow: serde_json::Map<String, serde_json::Value>,
pub tool_refs: Vec<AgentToolRef>,
pub tool_refs_set: bool,
}
tokio::task_local! {
static REQUEST_CONTEXT: Option<v1::RequestContext>;
}
impl Request {
pub fn connection_param(&self, name: &str) -> Option<&str> {
self.connection_params.get(name).map(String::as_str)
}
}
pub fn current_request_context() -> Option<v1::RequestContext> {
REQUEST_CONTEXT.try_with(Clone::clone).ok().flatten()
}
pub fn current_native_request_context() -> Option<crate::app::RequestContext> {
current_request_context().map(native_request_context)
}
fn native_request_context(value: v1::RequestContext) -> crate::app::RequestContext {
use crate::codec::app::{from_wire_agent_tool_ref, from_wire_subject_context};
use crate::codec::support::from_wire_struct;
crate::app::RequestContext {
subject: value.subject.map(from_wire_subject_context),
credential: value
.credential
.map(|credential| crate::app::CredentialContext {
mode: credential.mode,
subject_id: credential.subject_id,
connection: credential.connection,
instance: credential.instance,
}),
access: value.access.map(|access| crate::app::AccessContext {
policy: access.policy,
role: access.role,
}),
workflow: value.workflow.map(from_wire_struct),
host: value.host.map(|host| crate::app::HostContext {
public_base_url: host.public_base_url,
}),
agent_subject: value.agent_subject.map(from_wire_subject_context),
caller: value.caller.map(|caller| crate::app::ProviderContext {
kind: caller.kind,
name: caller.name,
}),
invocation: value
.invocation
.map(|invocation| crate::app::InvocationContext {
request_id: invocation.request_id,
depth: invocation.depth,
call_chain: invocation.call_chain,
surface: invocation.surface,
internal_connection_access: invocation.internal_connection_access,
connection: invocation.connection,
}),
tool_refs: value
.tool_refs
.into_iter()
.map(from_wire_agent_tool_ref)
.collect(),
tool_refs_set: value.tool_refs_set,
request_meta: value
.request_meta
.map(|meta| crate::app::RequestMetaContext {
client_ip: meta.client_ip,
remote_addr: meta.remote_addr,
user_agent: meta.user_agent,
}),
agent: value.agent.map(|agent| crate::app::AgentInvocationContext {
provider_name: agent.provider_name,
session_id: agent.session_id,
turn_id: agent.turn_id,
}),
}
}
pub async fn with_request_context<F>(context: Option<v1::RequestContext>, future: F) -> F::Output
where
F: Future,
{
REQUEST_CONTEXT.scope(context, future).await
}
pub(crate) async fn scope_request_context<F>(
context: Option<v1::RequestContext>,
future: F,
) -> F::Output
where
F: Future,
{
with_request_context(context, future).await
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct HTTPSubjectRequest {
pub binding: String,
pub method: String,
pub path: String,
pub content_type: String,
pub headers: BTreeMap<String, Vec<String>>,
pub query: BTreeMap<String, Vec<String>>,
pub params: serde_json::Map<String, serde_json::Value>,
pub raw_body: Vec<u8>,
pub security_scheme: String,
pub verified_subject: String,
pub verified_claims: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Response<T> {
pub status: Option<u16>,
pub headers: BTreeMap<String, Vec<String>>,
pub body: T,
}
impl<T> Response<T> {
pub fn new(status: u16, body: T) -> Self {
Self {
status: Some(status),
headers: BTreeMap::new(),
body,
}
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers
.entry(name.into())
.or_default()
.push(value.into());
self
}
}
pub fn ok<T>(body: T) -> Response<T> {
Response::new(200, body)
}
pub trait IntoResponse<T> {
fn into_response(self) -> Response<T>;
}
impl<T> IntoResponse<T> for Response<T> {
fn into_response(self) -> Response<T> {
self
}
}
impl<T> IntoResponse<T> for T {
fn into_response(self) -> Response<T> {
ok(self)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RuntimeMetadata {
pub name: String,
pub display_name: String,
pub description: String,
pub version: String,
}
#[async_trait]
pub trait Provider: Send + Sync + 'static {
async fn configure(
&self,
_name: &str,
_config: serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
Ok(())
}
fn metadata(&self) -> Option<RuntimeMetadata> {
None
}
fn warnings(&self) -> Vec<String> {
Vec::new()
}
async fn health_check(&self) -> Result<()> {
Ok(())
}
async fn start(&self) -> Result<()> {
Ok(())
}
fn supports_session_catalog(&self) -> bool {
false
}
async fn catalog_for_request(&self, _request: &Request) -> Result<Option<Catalog>> {
Ok(None)
}
async fn resolve_http_subject(
&self,
_request: HTTPSubjectRequest,
_context: &Request,
) -> Result<Option<Subject>> {
Ok(None)
}
async fn close(&self) -> Result<()> {
Ok(())
}
}
impl From<Infallible> for Error {
fn from(_value: Infallible) -> Self {
Error::internal("unreachable infallible error")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol;
#[tokio::test]
async fn converts_fully_populated_request_context() {
let wire = v1::RequestContext {
subject: Some(v1::SubjectContext {
id: "user:ada".to_string(),
credential_subject_id: "user:cred".to_string(),
email: "ada@example.test".to_string(),
display_name: "Ada".to_string(),
scopes: vec!["repo:read".to_string()],
permissions: vec![v1::SubjectPermissionContext {
app: "github".to_string(),
operations: vec!["issues.get".to_string()],
all_operations: false,
}],
}),
credential: Some(v1::CredentialContext {
mode: "user".to_string(),
subject_id: "user:cred".to_string(),
connection: "work".to_string(),
instance: "primary".to_string(),
}),
access: Some(v1::AccessContext {
policy: "default".to_string(),
role: "admin".to_string(),
}),
workflow: Some(
protocol::struct_from_json(serde_json::json!({
"runId": "run-1",
"trigger": { "activationId": "act-1" },
}))
.expect("workflow struct"),
),
host: Some(v1::HostContext {
public_base_url: "https://gestalt.example.test".to_string(),
}),
agent_subject: Some(v1::SubjectContext {
id: "agent:caller".to_string(),
..Default::default()
}),
caller: Some(v1::ProviderContext {
kind: "app".to_string(),
name: "hermes".to_string(),
}),
invocation: Some(v1::InvocationContext {
request_id: "req-1".to_string(),
depth: 2,
call_chain: vec!["hermes".to_string(), "github".to_string()],
surface: "mcp".to_string(),
internal_connection_access: true,
connection: "work".to_string(),
}),
tool_refs: vec![v1::AgentToolRef {
app: "slack".to_string(),
operation: "chat.postMessage".to_string(),
connection: "workspace".to_string(),
instance: "primary".to_string(),
title: "Send Slack message".to_string(),
description: "Post a Slack message".to_string(),
credential_mode: "user".to_string(),
system: "slack".to_string(),
run_as: Some(v1::SubjectContext {
id: "user:run-as".to_string(),
..Default::default()
}),
}],
tool_refs_set: true,
request_meta: Some(v1::RequestMetaContext {
client_ip: "203.0.113.7".to_string(),
remote_addr: "203.0.113.7:443".to_string(),
user_agent: "gestalt-test".to_string(),
}),
agent: Some(v1::AgentInvocationContext {
provider_name: "openai".to_string(),
session_id: "session-1".to_string(),
turn_id: "turn-1".to_string(),
}),
};
let native = with_request_context(Some(wire), async { current_native_request_context() })
.await
.expect("native context");
assert_eq!(
native,
crate::app::RequestContext {
subject: Some(crate::app::SubjectContext {
id: "user:ada".to_string(),
credential_subject_id: "user:cred".to_string(),
email: "ada@example.test".to_string(),
display_name: "Ada".to_string(),
scopes: vec!["repo:read".to_string()],
permissions: vec![crate::app::SubjectPermissionContext {
app: "github".to_string(),
operations: vec!["issues.get".to_string()],
all_operations: false,
}],
}),
credential: Some(crate::app::CredentialContext {
mode: "user".to_string(),
subject_id: "user:cred".to_string(),
connection: "work".to_string(),
instance: "primary".to_string(),
}),
access: Some(crate::app::AccessContext {
policy: "default".to_string(),
role: "admin".to_string(),
}),
workflow: serde_json::json!({
"runId": "run-1",
"trigger": { "activationId": "act-1" },
})
.as_object()
.cloned(),
host: Some(crate::app::HostContext {
public_base_url: "https://gestalt.example.test".to_string(),
}),
agent_subject: Some(crate::app::SubjectContext {
id: "agent:caller".to_string(),
..Default::default()
}),
caller: Some(crate::app::ProviderContext {
kind: "app".to_string(),
name: "hermes".to_string(),
}),
invocation: Some(crate::app::InvocationContext {
request_id: "req-1".to_string(),
depth: 2,
call_chain: vec!["hermes".to_string(), "github".to_string()],
surface: "mcp".to_string(),
internal_connection_access: true,
connection: "work".to_string(),
}),
tool_refs: vec![crate::app::AgentToolRef {
app: "slack".to_string(),
operation: "chat.postMessage".to_string(),
connection: "workspace".to_string(),
instance: "primary".to_string(),
title: "Send Slack message".to_string(),
description: "Post a Slack message".to_string(),
credential_mode: "user".to_string(),
system: "slack".to_string(),
run_as: Some(crate::app::SubjectContext {
id: "user:run-as".to_string(),
..Default::default()
}),
}],
tool_refs_set: true,
request_meta: Some(crate::app::RequestMetaContext {
client_ip: "203.0.113.7".to_string(),
remote_addr: "203.0.113.7:443".to_string(),
user_agent: "gestalt-test".to_string(),
}),
agent: Some(crate::app::AgentInvocationContext {
provider_name: "openai".to_string(),
session_id: "session-1".to_string(),
turn_id: "turn-1".to_string(),
}),
}
);
}
#[tokio::test]
async fn converts_sparse_request_context() {
let native = with_request_context(Some(v1::RequestContext::default()), async {
current_native_request_context()
})
.await
.expect("native context");
assert_eq!(native, crate::app::RequestContext::default());
}
#[test]
fn returns_none_outside_request_scope() {
assert!(current_native_request_context().is_none());
}
}