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 Kv,
9 Queue,
10 Key,
11 Ai,
12 Sandbox,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct RemoteBindingDefinition {
18 pub resource_type: &'static str,
19 pub permission_set: &'static str,
20 pub kind: RemoteBindingKind,
21 pub description: &'static str,
22 pub setup_support_resource_types: &'static [&'static str],
25 pub revision: u32,
28}
29
30const DEFINITIONS: &[RemoteBindingDefinition] = &[
31 RemoteBindingDefinition {
32 resource_type: "storage",
33 permission_set: "storage/remote-data-write",
34 kind: RemoteBindingKind::Storage,
35 description: "Read and write objects in this storage resource",
36 setup_support_resource_types: &[
37 "azure_resource_group",
38 "azure_storage_account",
39 "service_activation",
40 ],
41 revision: 1,
42 },
43 RemoteBindingDefinition {
44 resource_type: "queue",
45 permission_set: "queue/publish",
46 kind: RemoteBindingKind::Queue,
47 description: "Send messages to this queue",
48 setup_support_resource_types: &["azure_resource_group", "azure_service_bus_namespace", "service_activation"],
49 revision: 1,
50 },
51 RemoteBindingDefinition {
52 resource_type: "kv",
53 permission_set: "kv/remote-data-write",
54 kind: RemoteBindingKind::Kv,
55 description: "Read and write entries in this key-value store",
56 setup_support_resource_types: &[
57 "azure_resource_group",
58 "azure_storage_account",
59 "service_activation",
60 ],
61 revision: 1,
62 },
63 RemoteBindingDefinition {
64 resource_type: "key",
65 permission_set: "key/remote-cryptography",
66 kind: RemoteBindingKind::Key,
67 description: "Encrypt and decrypt small values with this key",
68 setup_support_resource_types: &["azure_resource_group", "service_activation"],
69 revision: 1,
70 },
71 RemoteBindingDefinition {
72 resource_type: "ai",
73 permission_set: "ai/invoke",
74 kind: RemoteBindingKind::Ai,
75 description: "Invoke models through this AI resource",
76 setup_support_resource_types: &["azure_resource_group", "service_activation"],
77 revision: 1,
78 },
79 RemoteBindingDefinition {
80 resource_type: "sandbox",
81 permission_set: "sandbox/remote-execute",
82 kind: RemoteBindingKind::Sandbox,
83 description:
84 "Create and terminate sandboxes in this sandbox resource, and run arbitrary code inside them",
85 setup_support_resource_types: &[],
88 revision: 1,
89 },
90];
91
92pub fn remote_binding_definition(
93 resource_type: &ResourceType,
94) -> Option<&'static RemoteBindingDefinition> {
95 DEFINITIONS
96 .iter()
97 .find(|definition| definition.resource_type == resource_type.as_ref())
98}
99
100pub fn remote_binding_for_entry(entry: &ResourceEntry) -> Option<&'static RemoteBindingDefinition> {
103 let resource_type = entry.config.resource_type();
104 (entry.remote_access
105 && ownership_policy_for_resource_type(resource_type.as_ref())
106 .emits_setup_scaffolding(entry.lifecycle))
107 .then(|| remote_binding_definition(&resource_type))
108 .flatten()
109}
110
111pub fn remote_binding_undeliverable_reason(entry: &ResourceEntry) -> Option<&'static str> {
134 remote_binding_for_entry(entry)?;
135 let sandbox = entry.config.downcast_ref::<Sandbox>()?;
136
137 if sandbox.privileged_supervisor.is_some() {
138 return Some("a remotely published sandbox cannot declare privilegedSupervisor; the raw grant can start retained image versions with a different command identity or egress policy; use an ordinary workload binding");
139 }
140
141 if !matches!(sandbox.egress, SandboxEgress::Allow) {
142 return Some(
143 "a remotely published sandbox must declare egress 'allow'; the remote grant either \
144 cannot pass a declared connector or lets its holder create sandboxes that ignore the \
145 declared policy, so the declaration would not bound the remote caller",
146 );
147 }
148
149 if !sandbox.preview_ports.is_empty() {
150 return Some(
151 "a remotely published sandbox must declare no previewPorts; the sandbox token mint \
152 carries no port condition, so the list bounds a caller reaching the sandbox through \
153 its binding but not a holder of the remote credentials",
154 );
155 }
156
157 None
158}
159
160pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
162 remote_binding_undeliverable_reason(entry).is_none()
163}
164
165pub fn remote_binding_claims_management_set<'a>(
172 resources: impl IntoIterator<Item = &'a ResourceEntry>,
173 permission_set_id: &str,
174 reaches_a_sandbox: impl Fn() -> bool,
175) -> bool {
176 resources.into_iter().any(|entry| {
177 remote_binding_for_entry(entry).is_some_and(|definition| {
178 permission_set_id == definition.permission_set
179 || (definition.kind == RemoteBindingKind::Sandbox && reaches_a_sandbox())
180 })
181 })
182}
183
184pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
185 DEFINITIONS
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLifecyclePolicy, SandboxLimits};
192
193 fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
194 let sandbox = Sandbox::new("agent-sbx".to_string())
195 .code(SandboxCode::Image {
196 image: "ubuntu".to_string(),
197 })
198 .limits(SandboxLimits {
199 cpu: "1".to_string(),
200 memory: "2Gi".to_string(),
201 disk: "20Gi".to_string(),
202 max_processes: None,
203 })
204 .egress(egress)
205 .lifecycle(SandboxLifecyclePolicy {
206 max_lifetime_seconds: None,
207 idle_pause_seconds: None,
208 })
209 .preview_ports(preview_ports)
210 .build();
211
212 ResourceEntry {
213 enabled_when: None,
214 config: crate::Resource::new(sandbox),
215 dependencies: Vec::new(),
216 lifecycle: ResourceLifecycle::Frozen,
217 remote_access: true,
218 }
219 }
220
221 #[test]
222 fn a_remote_grant_cannot_bypass_supervision_through_a_retained_version() {
223 for egress in [
224 SandboxEgress::Allow,
225 SandboxEgress::Deny,
226 SandboxEgress::AllowDomains {
227 domains: vec!["example.com".to_string()],
228 },
229 ] {
230 let mut entry = remote_sandbox(egress, vec![]);
231 let mut sandbox = entry
232 .config
233 .downcast_ref::<Sandbox>()
234 .expect("sandbox")
235 .clone();
236 sandbox.privileged_supervisor =
237 Some(crate::SandboxPrivilegedSupervisor { command_uid: 60001 });
238 entry.config = crate::Resource::new(sandbox);
239 assert!(remote_binding_undeliverable_reason(&entry)
240 .expect("raw version-wide grant is unsafe")
241 .contains("privilegedSupervisor"));
242 entry.remote_access = false;
243 assert_eq!(
244 remote_binding_undeliverable_reason(&entry),
245 None,
246 "ordinary bindings select the active version"
247 );
248 }
249 }
250
251 #[test]
254 fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
255 assert!(remote_binding_is_deliverable(&remote_sandbox(
256 SandboxEgress::Allow,
257 Vec::new()
258 )));
259 }
260
261 #[test]
264 fn a_remote_sandbox_declaring_ports_is_refused() {
265 let reason =
266 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
267 .expect("a declared port list is not deliverable to a remote caller");
268
269 assert!(
270 reason.contains("previewPorts"),
271 "the refusal must name the field the user declared"
272 );
273 }
274
275 #[test]
278 fn a_sandbox_with_no_remote_binding_may_declare_ports() {
279 let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
280 entry.remote_access = false;
281
282 assert_eq!(remote_binding_undeliverable_reason(&entry), None);
283 assert!(remote_binding_is_deliverable(&entry));
284 }
285
286 #[test]
289 fn each_undeliverable_declaration_answers_in_its_own_terms() {
290 let egress =
291 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Deny, Vec::new()))
292 .expect("a restricted egress is not deliverable");
293 let ports =
294 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
295 .expect("a declared port list is not deliverable");
296
297 assert_ne!(egress, ports, "one reason cannot stand in for the other");
298 assert!(egress.contains("egress"));
299 }
300}