use crate::{
ownership_policy_for_resource_type, ResourceEntry, ResourceType, Sandbox, SandboxEgress,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteBindingKind {
Storage,
Kv,
Queue,
Key,
Ai,
Sandbox,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RemoteBindingDefinition {
pub resource_type: &'static str,
pub permission_set: &'static str,
pub kind: RemoteBindingKind,
pub description: &'static str,
pub setup_support_resource_types: &'static [&'static str],
pub revision: u32,
}
const DEFINITIONS: &[RemoteBindingDefinition] = &[
RemoteBindingDefinition {
resource_type: "storage",
permission_set: "storage/remote-data-write",
kind: RemoteBindingKind::Storage,
description: "Read and write objects in this storage resource",
setup_support_resource_types: &[
"azure_resource_group",
"azure_storage_account",
"service_activation",
],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "queue",
permission_set: "queue/publish",
kind: RemoteBindingKind::Queue,
description: "Send messages to this queue",
setup_support_resource_types: &["azure_resource_group", "azure_service_bus_namespace", "service_activation"],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "kv",
permission_set: "kv/remote-data-write",
kind: RemoteBindingKind::Kv,
description: "Read and write entries in this key-value store",
setup_support_resource_types: &[
"azure_resource_group",
"azure_storage_account",
"service_activation",
],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "key",
permission_set: "key/remote-cryptography",
kind: RemoteBindingKind::Key,
description: "Encrypt and decrypt small values with this key",
setup_support_resource_types: &["azure_resource_group", "service_activation"],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "ai",
permission_set: "ai/invoke",
kind: RemoteBindingKind::Ai,
description: "Invoke models through this AI resource",
setup_support_resource_types: &["azure_resource_group", "service_activation"],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "sandbox",
permission_set: "sandbox/remote-execute",
kind: RemoteBindingKind::Sandbox,
description:
"Create and terminate sandboxes in this sandbox resource, and run arbitrary code inside them",
setup_support_resource_types: &[],
revision: 1,
},
];
pub fn remote_binding_definition(
resource_type: &ResourceType,
) -> Option<&'static RemoteBindingDefinition> {
DEFINITIONS
.iter()
.find(|definition| definition.resource_type == resource_type.as_ref())
}
pub fn remote_binding_for_entry(entry: &ResourceEntry) -> Option<&'static RemoteBindingDefinition> {
let resource_type = entry.config.resource_type();
(entry.remote_access
&& ownership_policy_for_resource_type(resource_type.as_ref())
.emits_setup_scaffolding(entry.lifecycle))
.then(|| remote_binding_definition(&resource_type))
.flatten()
}
pub fn remote_binding_undeliverable_reason(entry: &ResourceEntry) -> Option<&'static str> {
remote_binding_for_entry(entry)?;
let sandbox = entry.config.downcast_ref::<Sandbox>()?;
if sandbox.privileged_supervisor.is_some() {
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");
}
if !matches!(sandbox.egress, SandboxEgress::Allow) {
return Some(
"a remotely published sandbox must declare egress 'allow'; the remote grant either \
cannot pass a declared connector or lets its holder create sandboxes that ignore the \
declared policy, so the declaration would not bound the remote caller",
);
}
if !sandbox.preview_ports.is_empty() {
return Some(
"a remotely published sandbox must declare no previewPorts; the sandbox token mint \
carries no port condition, so the list bounds a caller reaching the sandbox through \
its binding but not a holder of the remote credentials",
);
}
None
}
pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
remote_binding_undeliverable_reason(entry).is_none()
}
pub fn remote_binding_claims_management_set<'a>(
resources: impl IntoIterator<Item = &'a ResourceEntry>,
permission_set_id: &str,
reaches_a_sandbox: impl Fn() -> bool,
) -> bool {
resources.into_iter().any(|entry| {
remote_binding_for_entry(entry).is_some_and(|definition| {
permission_set_id == definition.permission_set
|| (definition.kind == RemoteBindingKind::Sandbox && reaches_a_sandbox())
})
})
}
pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
DEFINITIONS
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLifecyclePolicy, SandboxLimits};
fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
let sandbox = Sandbox::new("agent-sbx".to_string())
.code(SandboxCode::Image {
image: "ubuntu".to_string(),
})
.limits(SandboxLimits {
cpu: "1".to_string(),
memory: "2Gi".to_string(),
disk: "20Gi".to_string(),
max_processes: None,
})
.egress(egress)
.lifecycle(SandboxLifecyclePolicy {
max_lifetime_seconds: None,
idle_pause_seconds: None,
})
.preview_ports(preview_ports)
.build();
ResourceEntry {
enabled_when: None,
config: crate::Resource::new(sandbox),
dependencies: Vec::new(),
lifecycle: ResourceLifecycle::Frozen,
remote_access: true,
}
}
#[test]
fn a_remote_grant_cannot_bypass_supervision_through_a_retained_version() {
for egress in [
SandboxEgress::Allow,
SandboxEgress::Deny,
SandboxEgress::AllowDomains {
domains: vec!["example.com".to_string()],
},
] {
let mut entry = remote_sandbox(egress, vec![]);
let mut sandbox = entry
.config
.downcast_ref::<Sandbox>()
.expect("sandbox")
.clone();
sandbox.privileged_supervisor =
Some(crate::SandboxPrivilegedSupervisor { command_uid: 60001 });
entry.config = crate::Resource::new(sandbox);
assert!(remote_binding_undeliverable_reason(&entry)
.expect("raw version-wide grant is unsafe")
.contains("privilegedSupervisor"));
entry.remote_access = false;
assert_eq!(
remote_binding_undeliverable_reason(&entry),
None,
"ordinary bindings select the active version"
);
}
}
#[test]
fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
assert!(remote_binding_is_deliverable(&remote_sandbox(
SandboxEgress::Allow,
Vec::new()
)));
}
#[test]
fn a_remote_sandbox_declaring_ports_is_refused() {
let reason =
remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
.expect("a declared port list is not deliverable to a remote caller");
assert!(
reason.contains("previewPorts"),
"the refusal must name the field the user declared"
);
}
#[test]
fn a_sandbox_with_no_remote_binding_may_declare_ports() {
let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
entry.remote_access = false;
assert_eq!(remote_binding_undeliverable_reason(&entry), None);
assert!(remote_binding_is_deliverable(&entry));
}
#[test]
fn each_undeliverable_declaration_answers_in_its_own_terms() {
let egress =
remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Deny, Vec::new()))
.expect("a restricted egress is not deliverable");
let ports =
remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
.expect("a declared port list is not deliverable");
assert_ne!(egress, ports, "one reason cannot stand in for the other");
assert!(egress.contains("egress"));
}
}