1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//! Neutral contracts for provider credentials and user connections.
use crate::runtime::error::Result;
use crate::runtime::typed_id::SessionId;
use async_trait::async_trait;
use uuid::Uuid;
/// Provider credentials resolved for tool-side API clients.
#[derive(Debug, Clone)]
pub struct ProviderCredentials {
pub api_key: String,
pub base_url: Option<String>,
}
#[async_trait]
pub trait ProviderCredentialStore: Send + Sync {
/// Resolve a trusted catalog selection. Never falls back to another account.
async fn get_decision_model(
&self,
_model_id: Option<&str>,
_session_id: SessionId,
) -> Result<Option<DecisionModelBinding>> {
Err(crate::runtime::error::AgentLoopError::store(
"Decision model resolution is unavailable",
))
}
/// Who answers deployment-owned decision checks (guardrail `jev` checks,
/// Slack relevance) for this session's org.
///
/// `Deployment` keeps the deployment's decisions. `Organization` carries the
/// org's decision default. An org that opted in but has no usable model is
/// an error, never `Deployment`: callers fail open or stay silent instead of
/// spending deployment keys (THREAT[TM-LLM-037]). Hosts without org settings
/// keep the deployment.
async fn get_system_decision_model(
&self,
_session_id: SessionId,
) -> Result<SystemDecisionModel> {
Ok(SystemDecisionModel::Deployment)
}
/// Resolve default credentials for a provider type (for example `openai`).
///
/// Implementations may apply environment fallbacks internally, but tools
/// should never read provider env vars directly.
async fn get_default_provider_credentials(
&self,
provider_type: &str,
) -> Result<Option<ProviderCredentials>>;
}
/// An MCP credential and the concrete identity (`user` or `service`) whose
/// grant supplied it.
#[derive(Clone, PartialEq, Eq)]
pub struct McpResolvedCredential {
/// Decrypted bearer token.
pub token: String,
/// Identity whose grant the token came from.
pub acted_as: crate::runtime::mcp_server::McpServerActsAs,
}
impl std::fmt::Debug for McpResolvedCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpResolvedCredential")
.field("token", &"<redacted>")
.field("acted_as", &self.acted_as)
.finish()
}
}
/// The source an org picked for deployment-owned decision checks.
#[derive(Clone)]
pub enum SystemDecisionModel {
/// The deployment's decisions service answers.
Deployment,
/// The org's decision default answers, through the host's model boundary.
Organization(DecisionModelBinding),
}
/// Runs an org-selected decision model through the host's egress, budget, and
/// usage path, the same path the Jev tool takes.
#[async_trait]
pub trait DecisionModelExecutor: Send + Sync {
async fn evaluate(
&self,
binding: DecisionModelBinding,
request: crate::runtime::decisions::DecisionRequest,
context: &crate::runtime::tool_context::ToolContext,
) -> Result<crate::runtime::decisions::DecisionOutcome>;
}
/// Tool-context extension carrying the host's [`DecisionModelExecutor`].
pub struct DecisionModelExecutorExt(pub std::sync::Arc<dyn DecisionModelExecutor>);
/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
///
/// Instead of eagerly injecting tokens at session creation, tools call this
/// resolver when they need a token. If the user hasn't connected, returns None.
#[async_trait]
pub trait UserConnectionResolver: Send + Sync {
/// Bind credential resolution to a stored input-message invocation. Implementations
/// without this capability remain service-only and fail closed for consumer grants.
fn for_execution(
&self,
_input_message_id: Uuid,
) -> Option<std::sync::Arc<dyn UserConnectionResolver>> {
None
}
/// Bind a configured MCP attachment at a remote execution boundary.
fn for_mcp_operation(
&self,
_server_prefix: &str,
) -> Option<std::sync::Arc<dyn UserConnectionResolver>> {
None
}
/// Get a decrypted connection token for the given provider.
/// Returns None if the user has no connection for this provider.
async fn get_connection_token(
&self,
session_id: SessionId,
provider: &str,
) -> Result<Option<String>>;
/// Resolve a decrypted MCP connection token as a pure function of the
/// attachment's `actsAs`.
///
/// Acting identity is explicit configuration over one credential store:
/// none reads no grant; service reads the responding agent's service virtual
/// user; user reads the current invocation's end user. Neither identity
/// falls back to the other, and session ownership never selects a grant.
///
/// `Ok(None)` means "no credential", which callers surface as
/// `connection_required` rather than an unauthenticated request.
///
/// THREAT[TM-TOOL-041]: the default implementation is fail-closed on
/// purpose. A resolver that has not opted in must never silently fall back
/// to the identity-preferring lookup, because that is the substitution this
/// method exists to remove.
async fn get_mcp_connection_token(
&self,
_session_id: SessionId,
_provider: &str,
_acts_as: crate::runtime::mcp_server::McpServerActsAs,
) -> Result<Option<String>> {
Ok(None)
}
/// Resolve an MCP credential together with the identity that supplied it.
///
/// `user_or_service` tries the invoking user's grant first and falls back
/// to the agent's service grant; every other value reads exactly one store,
/// as [`Self::get_mcp_connection_token`] does. An unattended run has no
/// invoking user, so its user lookup is empty and it uses the service grant.
/// The returned `acted_as` is always concrete (`user` or `service`), so a
/// caller can record which account a call ran as.
async fn get_mcp_connection_credential(
&self,
session_id: SessionId,
provider: &str,
acts_as: crate::runtime::mcp_server::McpServerActsAs,
) -> Result<Option<McpResolvedCredential>> {
for identity in acts_as.resolution_order() {
if let Some(token) = self
.get_mcp_connection_token(session_id, provider, *identity)
.await?
{
return Ok(Some(McpResolvedCredential {
token,
acted_as: *identity,
}));
}
}
Ok(None)
}
/// Resolve the responding agent's own service-account API-key connection
/// for `provider`: its key and the provider metadata stored with it.
///
/// Reads only the agent's service virtual user. It never reads the
/// invoking end user's connections and never falls back to them, or to a
/// management user's. `Ok(None)` means "no service connection".
///
/// THREAT[TM-TOOL-041]: fail-closed by default, like
/// [`Self::get_mcp_connection_token`]; a resolver that has not opted in
/// must not substitute the identity-preferring lookup.
async fn get_service_api_key_connection(
&self,
_session_id: SessionId,
_provider: &str,
) -> Result<Option<ServiceApiKeyConnection>> {
Ok(None)
}
/// Invalidate an MCP credential after the remote server rejects it.
///
/// Implementations that own persistent grants can remove the credential
/// selected by `acts_as` and its credential-scoped tool cache. The default
/// is a no-op. Implementations must preserve a replacement grant when its
/// fingerprint differs from the credential the remote server rejected.
async fn invalidate_mcp_connection(
&self,
_session_id: SessionId,
_provider: &str,
_acts_as: crate::runtime::mcp_server::McpServerActsAs,
_rejected_credential_fingerprint: &str,
) -> Result<()> {
Ok(())
}
/// Resolve the user ID of the connection used for a session/provider pair.
///
/// This is used by leased resources to bind cleanup to the same provider
/// identity that created the remote resource.
async fn get_connection_user(
&self,
_session_id: SessionId,
_provider: &str,
) -> Result<Option<Uuid>> {
Ok(None)
}
/// Resolve a provider token for a specific user.
///
/// Cleanup workers use this to avoid "first org member wins" behavior when
/// cleaning resources created by a specific provider connection owner.
async fn get_connection_token_for_user(
&self,
_user_id: Uuid,
_provider: &str,
) -> Result<Option<String>> {
Ok(None)
}
/// Resolve one exact connection owned by the given virtual user. Sandbox
/// lifecycle uses this for organization accounts so provisioning and later
/// cleanup cannot drift to a different account for the same provider.
async fn get_connection_token_for_connection(
&self,
_connection_id: Uuid,
_virtual_user_id: Uuid,
_provider: &str,
) -> Result<Option<String>> {
Ok(None)
}
/// Get provider-specific metadata stored alongside the connection.
/// Returns None if no metadata is stored or no connection exists.
async fn get_connection_metadata(
&self,
_session_id: SessionId,
_provider: &str,
) -> Result<Option<serde_json::Value>> {
Ok(None)
}
}
/// An API key held by an agent's own service account, with the provider
/// metadata stored beside it (for example an AgentMail inbox id).
#[derive(Clone)]
pub struct ServiceApiKeyConnection {
pub api_key: String,
pub metadata: Option<serde_json::Value>,
}
impl std::fmt::Debug for ServiceApiKeyConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceApiKeyConnection")
.field("api_key", &"[redacted]")
.field("metadata", &self.metadata)
.finish()
}
}
/// Resolved account material stays internal to the host boundary.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct DecisionModelBinding {
pub model_id: String,
pub provider_id: String,
pub provider_type: String,
pub model: String,
pub profile_key: String,
pub api_key: String,
pub base_url: Option<String>,
pub headers: std::collections::BTreeMap<String, String>,
}