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> {
99 remote_binding_for_entry(entry)?;
100 let sandbox = entry.config.downcast_ref::<Sandbox>()?;
101
102 if !matches!(sandbox.egress, SandboxEgress::Allow) {
103 return Some(
104 "a remotely published sandbox must declare egress 'allow'; a sandbox that routes its \
105 traffic through an egress connector cannot be reached remotely",
106 );
107 }
108
109 if !sandbox.preview_ports.is_empty() {
110 return Some(
111 "a remotely published sandbox must declare no previewPorts; the session token mint \
112 carries no port condition, so the list bounds a caller reaching the sandbox through \
113 its binding but not a holder of the remote credentials",
114 );
115 }
116
117 None
118}
119
120pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
122 remote_binding_undeliverable_reason(entry).is_none()
123}
124
125pub fn remote_binding_claims_management_set<'a>(
132 resources: impl IntoIterator<Item = &'a ResourceEntry>,
133 permission_set_id: &str,
134 reaches_a_session: impl Fn() -> bool,
135) -> bool {
136 resources.into_iter().any(|entry| {
137 remote_binding_for_entry(entry).is_some_and(|definition| {
138 permission_set_id == definition.permission_set
139 || (definition.kind == RemoteBindingKind::Sandbox && reaches_a_session())
140 })
141 })
142}
143
144pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
145 DEFINITIONS
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLimits, SandboxSessionPolicy};
152
153 fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
154 let sandbox = Sandbox::new("agent-sbx".to_string())
155 .code(SandboxCode::Image {
156 image: "ubuntu".to_string(),
157 })
158 .limits(SandboxLimits {
159 cpu: "1".to_string(),
160 memory: "2Gi".to_string(),
161 disk: "20Gi".to_string(),
162 max_processes: None,
163 })
164 .egress(egress)
165 .session(SandboxSessionPolicy {
166 max_lifetime_seconds: None,
167 idle_suspend_seconds: None,
168 })
169 .preview_ports(preview_ports)
170 .build();
171
172 ResourceEntry {
173 enabled_when: None,
174 config: crate::Resource::new(sandbox),
175 dependencies: Vec::new(),
176 lifecycle: ResourceLifecycle::Frozen,
177 remote_access: true,
178 }
179 }
180
181 #[test]
184 fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
185 assert!(remote_binding_is_deliverable(&remote_sandbox(
186 SandboxEgress::Allow,
187 Vec::new()
188 )));
189 }
190
191 #[test]
194 fn a_remote_sandbox_declaring_ports_is_refused() {
195 let reason =
196 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
197 .expect("a declared port list is not deliverable to a remote caller");
198
199 assert!(
200 reason.contains("previewPorts"),
201 "the refusal must name the field the user declared"
202 );
203 }
204
205 #[test]
208 fn a_sandbox_with_no_remote_binding_may_declare_ports() {
209 let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
210 entry.remote_access = false;
211
212 assert_eq!(remote_binding_undeliverable_reason(&entry), None);
213 assert!(remote_binding_is_deliverable(&entry));
214 }
215
216 #[test]
219 fn each_undeliverable_declaration_answers_in_its_own_terms() {
220 let egress =
221 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Deny, Vec::new()))
222 .expect("a restricted egress is not deliverable");
223 let ports =
224 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
225 .expect("a declared port list is not deliverable");
226
227 assert_ne!(egress, ports, "one reason cannot stand in for the other");
228 assert!(egress.contains("egress"));
229 }
230}