1use crate::{
2 ownership_policy_for_resource_type, ResourceEntry, ResourceType, Sandbox, SandboxEgress,
3};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum RemoteBindingKind {
7 Storage,
8 Key,
9 Ai,
10 Sandbox,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct RemoteBindingDefinition {
16 pub resource_type: &'static str,
17 pub permission_set: &'static str,
18 pub kind: RemoteBindingKind,
19 pub description: &'static str,
20 pub setup_support_resource_types: &'static [&'static str],
23 pub revision: u32,
26}
27
28const DEFINITIONS: &[RemoteBindingDefinition] = &[
29 RemoteBindingDefinition {
30 resource_type: "storage",
31 permission_set: "storage/remote-data-write",
32 kind: RemoteBindingKind::Storage,
33 description: "Read and write objects in this storage resource",
34 setup_support_resource_types: &[
35 "azure_resource_group",
36 "azure_storage_account",
37 "service_activation",
38 ],
39 revision: 1,
40 },
41 RemoteBindingDefinition {
42 resource_type: "key",
43 permission_set: "key/remote-cryptography",
44 kind: RemoteBindingKind::Key,
45 description: "Encrypt and decrypt small values with this key",
46 setup_support_resource_types: &["azure_resource_group", "service_activation"],
47 revision: 1,
48 },
49 RemoteBindingDefinition {
50 resource_type: "ai",
51 permission_set: "ai/invoke",
52 kind: RemoteBindingKind::Ai,
53 description: "Invoke models through this AI resource",
54 setup_support_resource_types: &["azure_resource_group", "service_activation"],
55 revision: 1,
56 },
57 RemoteBindingDefinition {
58 resource_type: "sandbox",
59 permission_set: "sandbox/remote-execute",
60 kind: RemoteBindingKind::Sandbox,
61 description:
62 "Create and terminate sessions in this sandbox, and run arbitrary code inside them",
63 setup_support_resource_types: &[],
66 revision: 1,
67 },
68];
69
70pub fn remote_binding_definition(
71 resource_type: &ResourceType,
72) -> Option<&'static RemoteBindingDefinition> {
73 DEFINITIONS
74 .iter()
75 .find(|definition| definition.resource_type == resource_type.as_ref())
76}
77
78pub fn remote_binding_for_entry(entry: &ResourceEntry) -> Option<&'static RemoteBindingDefinition> {
81 let resource_type = entry.config.resource_type();
82 (entry.remote_access
83 && ownership_policy_for_resource_type(resource_type.as_ref())
84 .emits_setup_scaffolding(entry.lifecycle))
85 .then(|| remote_binding_definition(&resource_type))
86 .flatten()
87}
88
89pub fn remote_binding_undeliverable_reason(entry: &ResourceEntry) -> Option<&'static str> {
112 remote_binding_for_entry(entry)?;
113 let sandbox = entry.config.downcast_ref::<Sandbox>()?;
114
115 if !matches!(sandbox.egress, SandboxEgress::Allow) {
116 return Some(
117 "a remotely published sandbox must declare egress 'allow'; the remote grant either \
118 cannot pass a declared connector or lets its holder create sessions that ignore the \
119 declared policy, so the declaration would not bound the remote caller",
120 );
121 }
122
123 if !sandbox.preview_ports.is_empty() {
124 return Some(
125 "a remotely published sandbox must declare no previewPorts; the session token mint \
126 carries no port condition, so the list bounds a caller reaching the sandbox through \
127 its binding but not a holder of the remote credentials",
128 );
129 }
130
131 None
132}
133
134pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
136 remote_binding_undeliverable_reason(entry).is_none()
137}
138
139pub fn remote_binding_claims_management_set<'a>(
146 resources: impl IntoIterator<Item = &'a ResourceEntry>,
147 permission_set_id: &str,
148 reaches_a_session: impl Fn() -> bool,
149) -> bool {
150 resources.into_iter().any(|entry| {
151 remote_binding_for_entry(entry).is_some_and(|definition| {
152 permission_set_id == definition.permission_set
153 || (definition.kind == RemoteBindingKind::Sandbox && reaches_a_session())
154 })
155 })
156}
157
158pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
159 DEFINITIONS
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLimits, SandboxSessionPolicy};
166
167 fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
168 let sandbox = Sandbox::new("agent-sbx".to_string())
169 .code(SandboxCode::Image {
170 image: "ubuntu".to_string(),
171 })
172 .limits(SandboxLimits {
173 cpu: "1".to_string(),
174 memory: "2Gi".to_string(),
175 disk: "20Gi".to_string(),
176 max_processes: None,
177 })
178 .egress(egress)
179 .session(SandboxSessionPolicy {
180 max_lifetime_seconds: None,
181 idle_suspend_seconds: None,
182 })
183 .preview_ports(preview_ports)
184 .build();
185
186 ResourceEntry {
187 enabled_when: None,
188 config: crate::Resource::new(sandbox),
189 dependencies: Vec::new(),
190 lifecycle: ResourceLifecycle::Frozen,
191 remote_access: true,
192 }
193 }
194
195 #[test]
198 fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
199 assert!(remote_binding_is_deliverable(&remote_sandbox(
200 SandboxEgress::Allow,
201 Vec::new()
202 )));
203 }
204
205 #[test]
208 fn a_remote_sandbox_declaring_ports_is_refused() {
209 let reason =
210 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
211 .expect("a declared port list is not deliverable to a remote caller");
212
213 assert!(
214 reason.contains("previewPorts"),
215 "the refusal must name the field the user declared"
216 );
217 }
218
219 #[test]
222 fn a_sandbox_with_no_remote_binding_may_declare_ports() {
223 let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
224 entry.remote_access = false;
225
226 assert_eq!(remote_binding_undeliverable_reason(&entry), None);
227 assert!(remote_binding_is_deliverable(&entry));
228 }
229
230 #[test]
233 fn each_undeliverable_declaration_answers_in_its_own_terms() {
234 let egress =
235 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Deny, Vec::new()))
236 .expect("a restricted egress is not deliverable");
237 let ports =
238 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
239 .expect("a declared port list is not deliverable");
240
241 assert_ne!(egress, ports, "one reason cannot stand in for the other");
242 assert!(egress.contains("egress"));
243 }
244}