Repository navigation
Expand file tree
/
Copy pathproject_binding.rs
More file actions
102 lines (99 loc) · 3.22 KB
/
Copy pathproject_binding.rs
File metadata and controls
102 lines (99 loc) · 3.22 KB
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
//! Exact, reauthorized Project observations for project-bound Plugin tools.
//!
//! Provider cwd is checked by the Runner against this target at invocation.
//! Neither this observation nor the opaque outer binding grants filesystem isolation.
use super::{GatewayError, PluginProjectTarget, ToolRuntime};
use crate::auth::AuthContext;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct BoundPluginProject {
pub(super) canonical_id: String,
pub(super) target: PluginProjectTarget,
}
/// Preflight the bound business Project before selecting permission policy.
/// Call dispatch rechecks it again after governance; a recorder never supplies it.
pub(super) async fn invocation_project(
runtime: &ToolRuntime,
request: &super::PluginToolCall,
auth: Option<&AuthContext>,
) -> Result<Option<BoundPluginProject>, GatewayError> {
if !matches!(
super::PluginOperation::from(request.action),
super::PluginOperation::Call
) {
return Ok(None);
}
let Some(binding) = request
.binding
.as_deref()
.and_then(|id| runtime.plugin_gateway.binding(id))
else {
return Ok(None);
};
let Some(project) = binding.project else {
return Ok(None);
};
let current = resolve_bound_project(
runtime,
&project.canonical_id,
&binding.client_id,
crate::auth::SCOPE_PROJECT_WRITE,
auth,
)
.await?;
if current != project {
return Err(GatewayError::local(
"plugin_project_changed",
"The described Project root changed; the call was not dispatched",
));
}
Ok(Some(current))
}
pub(super) async fn resolve_bound_project(
runtime: &ToolRuntime,
project: &str,
runner_id: &str,
scope: &str,
auth: Option<&AuthContext>,
) -> Result<BoundPluginProject, GatewayError> {
if !auth.is_some_and(|auth| auth.has_scope(scope)) {
return Err(GatewayError::local(
"insufficient_scope",
"Project-bound Plugin access requires the matching Project scope",
));
}
let resolved = runtime
.resolve_project_input_for_auth(project, auth)
.await
.map_err(|_| {
GatewayError::local(
"plugin_project_unavailable",
"The exact authorized Project is unavailable",
)
})?;
if resolved.config.client_id != runner_id {
return Err(GatewayError::local(
"plugin_project_mismatch",
"Project and Plugin Runner must match",
));
}
let project_id = crate::tool_runtime::runner_local_project_id(&resolved.resolved_id)
.ok_or_else(|| {
GatewayError::local(
"plugin_project_unavailable",
"The exact Runner Project is unavailable",
)
})?;
let root_fingerprint = resolved.root_fingerprint.ok_or_else(|| {
GatewayError::local(
"plugin_project_unavailable",
"The Project root identity is unavailable",
)
})?;
Ok(BoundPluginProject {
target: PluginProjectTarget {
project_id: project_id.to_string(),
root_fingerprint,
},
canonical_id: resolved.resolved_id,
})
}