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 sandbox.default_policy.is_some() {
142 return Some("a remotely published sandbox cannot carry defaultPolicy; use an ordinary workload binding that supports gateway policies");
143 }
144
145 if matches!(sandbox.egress, SandboxEgress::Internet) {
146 return Some("internet sandboxes cannot publish remoteAccess because the gateway admin endpoint is internal; use an in-deployment workload binding or retain legacy egress {mode:allow}");
147 }
148 if !matches!(sandbox.egress, SandboxEgress::Allow) {
149 return Some(
150 "a remotely published sandbox must declare egress 'allow'; the remote grant either \
151 cannot pass a declared connector or lets its holder create sandboxes that ignore the \
152 declared policy, so the declaration would not bound the remote caller",
153 );
154 }
155
156 if !sandbox.preview_ports.is_empty() {
157 return Some(
158 "a remotely published sandbox must declare no previewPorts; the sandbox token mint \
159 carries no port condition, so the list bounds a caller reaching the sandbox through \
160 its binding but not a holder of the remote credentials",
161 );
162 }
163
164 None
165}
166
167pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
169 remote_binding_undeliverable_reason(entry).is_none()
170}
171
172pub fn remote_binding_claims_management_set<'a>(
179 resources: impl IntoIterator<Item = &'a ResourceEntry>,
180 permission_set_id: &str,
181 reaches_a_sandbox: impl Fn() -> bool,
182) -> bool {
183 resources.into_iter().any(|entry| {
184 remote_binding_for_entry(entry).is_some_and(|definition| {
185 permission_set_id == definition.permission_set
186 || (definition.kind == RemoteBindingKind::Sandbox && reaches_a_sandbox())
187 })
188 })
189}
190
191pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
192 DEFINITIONS
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLifecyclePolicy, SandboxLimits};
199
200 fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
201 let sandbox = Sandbox::new("agent-sbx".to_string())
202 .code(SandboxCode::Image {
203 image: "ubuntu".to_string(),
204 bundled_from: None,
205 })
206 .limits(SandboxLimits {
207 cpu: "1".to_string(),
208 memory: "2Gi".to_string(),
209 disk: "20Gi".to_string(),
210 max_processes: None,
211 })
212 .egress(egress)
213 .lifecycle(SandboxLifecyclePolicy {
214 max_lifetime_seconds: None,
215 idle_pause_seconds: None,
216 })
217 .preview_ports(preview_ports)
218 .build();
219
220 ResourceEntry {
221 enabled_when: None,
222 config: crate::Resource::new(sandbox),
223 dependencies: Vec::new(),
224 lifecycle: ResourceLifecycle::Frozen,
225 display_name: None,
226 description: None,
227 remote_access: true,
228 }
229 }
230
231 #[test]
232 fn a_remote_grant_cannot_bypass_supervision_through_a_retained_version() {
233 for egress in [SandboxEgress::Allow, SandboxEgress::None] {
234 let mut entry = remote_sandbox(egress, vec![]);
235 let mut sandbox = entry
236 .config
237 .downcast_ref::<Sandbox>()
238 .expect("sandbox")
239 .clone();
240 sandbox.privileged_supervisor =
241 Some(crate::SandboxPrivilegedSupervisor { command_uid: 60001 });
242 entry.config = crate::Resource::new(sandbox);
243 assert!(remote_binding_undeliverable_reason(&entry)
244 .expect("raw version-wide grant is unsafe")
245 .contains("privilegedSupervisor"));
246 entry.remote_access = false;
247 assert_eq!(
248 remote_binding_undeliverable_reason(&entry),
249 None,
250 "ordinary bindings select the active version"
251 );
252 }
253 }
254
255 #[test]
258 fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
259 assert!(remote_binding_is_deliverable(&remote_sandbox(
260 SandboxEgress::Allow,
261 Vec::new()
262 )));
263 }
264
265 #[test]
268 fn a_remote_sandbox_declaring_ports_is_refused() {
269 let reason =
270 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
271 .expect("a declared port list is not deliverable to a remote caller");
272
273 assert!(
274 reason.contains("previewPorts"),
275 "the refusal must name the field the user declared"
276 );
277 }
278
279 #[test]
282 fn a_sandbox_with_no_remote_binding_may_declare_ports() {
283 let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
284 entry.remote_access = false;
285
286 assert_eq!(remote_binding_undeliverable_reason(&entry), None);
287 assert!(remote_binding_is_deliverable(&entry));
288 }
289
290 #[test]
293 fn each_undeliverable_declaration_answers_in_its_own_terms() {
294 let egress =
295 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::None, Vec::new()))
296 .expect("a restricted egress is not deliverable");
297 let ports =
298 remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
299 .expect("a declared port list is not deliverable");
300
301 assert_ne!(egress, ports, "one reason cannot stand in for the other");
302 assert!(egress.contains("egress"));
303 }
304}