Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions platform/sandbox/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ impl Approval {
}

impl Sandbox {
pub fn network(&self) -> &Network {
match self {
Self::Managed { network, .. } | Self::External { network } => network,
Self::Disabled => &Network::Allowed,
}
}

/// Prove that an existing resource needs no authority beyond this policy.
/// Differently written deny globs are conservatively treated as different;
/// no glob-language equivalence solver is needed for safe rebinding.
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/engine/src/reconcile/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub(crate) fn policy_view(tokens: Option<u64>) -> RunView {
sandbox: maka_sandbox::Sandbox::Disabled,
approval: maka_sandbox::Approval::Never,
},
network_approval_ceiling: None,
backend: BackendSpec::Model {
binding: ModelBinding {
connection_id: "policy".into(),
Expand Down
9 changes: 9 additions & 0 deletions plugins/agent/facts/src/durable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ pub struct RunSpec {
pub workspace: String,
pub permission_basis: PermissionBasis,
pub permission_ceiling: maka_sandbox::Permissions,
/// Inherited network ceiling for separately approved Host requests.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_approval_ceiling: Option<maka_sandbox::Network>,
pub backend: BackendSpec,
pub collaboration: CollaborationMode,
pub language: Language,
Expand Down Expand Up @@ -760,6 +763,12 @@ impl Fact {
}
}
impl RunSpec {
pub fn network_approval_ceiling(&self) -> &maka_sandbox::Network {
self.network_approval_ceiling
.as_ref()
.unwrap_or_else(|| self.permission_ceiling.sandbox.network())
}

pub fn content_refs(&self) -> Vec<&ContentRef> {
let mut refs = vec![&self.system_prompt, &self.tool_selection, &self.input];
match &self.backend {
Expand Down
4 changes: 4 additions & 0 deletions plugins/agent/native/src/owner/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ impl Owner {
maka_sandbox::Approval::Never
},
};
let inherited_network = parent.sandbox_floor().network();
let network_approval_ceiling = (inherited_network != permission_ceiling.sandbox.network())
.then(|| inherited_network.clone());
let system_prompt = basis.behavior.clone();
if let Some(prompt) = &system_prompt {
prompt.validate().map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -286,6 +289,7 @@ impl Owner {
workspace: configuration.workspace.path.clone(),
permission_basis: configuration.permission_basis(),
permission_ceiling,
network_approval_ceiling,
backend,
collaboration: basis.collaboration_mode,
language: basis.language,
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/native/src/owner/driver/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ impl Rig {
sandbox,
approval: maka_sandbox::Approval::Never,
},
network_approval_ceiling: None,
backend: match &external_backend {
Some(external) => BackendSpec::Executor {
selection: external.accepted.selection.clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ async fn tool_round(
"{request}"
);
let owner = rig.captured.lock().unwrap().clone().unwrap();
let approve_web = name == "WebFetch"
let approve_web = matches!(name, "WebFetch" | "WebSearch")
&& owner
.mutable_session(session)
.await?
Expand Down Expand Up @@ -418,13 +418,26 @@ async fn tool_matrix(code: bool) -> TestResult<()> {
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await?;
let address = format!("http://{}", listener.local_addr()?);
let web_calls = Arc::new(AtomicUsize::new(0));
let search_calls = Arc::new(AtomicUsize::new(0));
let mcp_calls = Arc::new(AtomicUsize::new(0));
let web = web_calls.clone();
let search = search_calls.clone();
let mcp = mcp_calls.clone();
let app = axum::Router::new()
.route("/page",axum::routing::get(move || {let calls = web.clone(); async move {
calls.fetch_add(1,Ordering::SeqCst); "web-acceptance-marker"
}}))
.route("/search", axum::routing::get(move |uri: axum::http::Uri| {
let search = search.clone();
async move {
assert_eq!(uri.query(), Some("q=Rust"));
search.fetch_add(1, Ordering::SeqCst);
(
[("content-type", "application/json")],
json!({"results":[{"url":"https://www.rust-lang.org/"}]}).to_string(),
)
}
}))
.route("/mcp",axum::routing::post(move |body: axum::body::Bytes| {
let calls = mcp.clone(); async move {
use axum::response::IntoResponse;
Expand All @@ -450,17 +463,42 @@ async fn tool_matrix(code: bool) -> TestResult<()> {
config["tools"] = json!([
{"name":"Read","description":"Read local file","target":{"kind":"import","name":"files"},"method":"readText","nestable":true,"result":"json","confirmation":"preapproved"},
{"name":"Write","description":"Write local file","target":{"kind":"import","name":"files"},"method":"write","nestable":true,"result":"json","confirmation":"preapproved"},
{"name":"WebFetch","description":"Fetch a URL","target":{"kind":"import","name":"web"},"method":"fetch","input":"web","nestable":true,"result":"json","confirmation":"preapproved"}
{"name":"WebFetch","description":"Fetch a URL","target":{"kind":"import","name":"web"},"method":"fetch","input":"web","nestable":true,"result":"json","confirmation":"preapproved"},
{"name":"WebSearch","description":"Search the web","target":{"kind":"import","name":"web"},"method":"search","input":"web","nestable":true,"result":"json","confirmation":"preapproved"}
]);
config["mcpServers"] = json!([{"name":"fixture","transport":{"type":"streamable_http","url":format!("{address}/mcp")},"preapproved":true}]);
}).await?;
std::fs::write(base.join("workspace/input.txt"),"local-acceptance-marker")?;
attach(&mut rig,&boot).await?;
let client = rig.client.as_ref().unwrap();
let web = call(client,"web","snapshot",json!({})).await?;
assert_eq!(call(client,"web","save",json!({"revision":web["revision"],"settings":{"enabled":true,"source":"model"}})).await?["kind"],"updated");
assert_eq!(call(client,"web","save",json!({"revision":web["revision"],"settings":{
"enabled":true,"source":"model","backend":{"kind":"custom","endpoint":{
"url":format!("{address}/search"),"method":"get","urlParameters":{},"body":{},"auth":null,
"query":{"location":"query","name":"q"},"limit":null,
"results":{"list":"/results","url":"/url","title":null,"snippet":null}
}}
}})).await?["kind"],"updated");
let model = model(client,&provider.url,code).await?;
let preset = preset(client,&model,"Keep the reviewer role.").await?;
let session = create(client, &base, &model, None).await?;
let id = session["id"].as_str().unwrap();
for requests in [true, false] {
let owner = rig.captured.lock().unwrap().clone().unwrap();
let revision = owner.mutable_session(id).await?.revision;
let changed = call(client, "agent", "metadata", json!({
"sessionId":id,"expectedRevision":revision,"title":"Sandboxed search","labels":[],
"permissions":{"settings":{"access":"workspace_write","requestPermissions":requests},"revokeGrants":true}
})).await?;
assert_eq!(changed["kind"], "committed", "{changed}");
let output = tool_round(&rig, &mut provider, id, code, "WebSearch", json!({"query":"Rust","limit":1})).await?;
if requests {
assert!(output.contains("https://www.rust-lang.org/"), "{output}");
} else {
assert!(output.contains("WebSearch network access was denied"), "{output}");
}
assert_eq!(search_calls.load(Ordering::SeqCst), 1, "{output}");
}
for selected in [None,Some(&preset)] {
std::fs::write(base.join("workspace/output.txt"),"unchanged-before-write")?;
let session = create(client,&base,&model,selected).await?;
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/native/src/owner/imports/history_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl Owner {
maka_sandbox::Approval::Never
},
},
network_approval_ceiling: None,
backend: BackendSpec::Historical {},
collaboration: configuration.collaboration_mode,
language: self.resolve_language(&settings),
Expand Down
4 changes: 4 additions & 0 deletions plugins/agent/native/src/owner/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ impl Owner {
.sandbox
.contains(&view.spec.permission_ceiling.sandbox)
.map_err(|error| error.to_string())?
&& request
.spec
.network_approval_ceiling()
.contains(view.spec.network_approval_ceiling())
&& view
.spec
.permission_ceiling
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/native/src/owner/queue/controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn same_execution(a: &RunSpec, b: &RunSpec) -> bool {
&& a.workspace == b.workspace
&& a.permission_basis == b.permission_basis
&& a.permission_ceiling == b.permission_ceiling
&& a.network_approval_ceiling() == b.network_approval_ceiling()
&& a.backend == b.backend
&& a.collaboration == b.collaboration
&& a.language == b.language
Expand Down
4 changes: 4 additions & 0 deletions plugins/agent/native/src/owner/steering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ impl Owner {
.sandbox_floor()
.contains(&target.spec.permission_ceiling.sandbox)
.map_err(|error| error.to_string())?
|| !parent
.sandbox_floor()
.network()
.contains(target.spec.network_approval_ceiling())
{
return Err("agent_steering_isolation_unavailable".into());
}
Expand Down
9 changes: 8 additions & 1 deletion plugins/agent/native/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -842,11 +842,18 @@ impl Service {
computer,
)
.await?;
let floor = if computer {
let mut floor = if computer {
access.workspace_sandbox_floor().await?
} else {
access.sandbox_floor(&context.call.origin).await?
};
if selection.input == ToolInput::Web
&& let maka_sandbox::Sandbox::Managed { network, .. } = &mut floor
{
// Host HTTP still requires a destination grant. Its inherited ceiling
// is independent of the workspace process sandbox's base network.
*network = access.spec().network_approval_ceiling().clone();
}
let request = services
.request_with_work_and_context(
Identity::Agent {
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/state/src/fold/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn spec(backend: BackendSpec, reference: &ContentRef) -> RunSpec {
sandbox: maka_sandbox::Sandbox::Disabled,
approval: maka_sandbox::Approval::Never,
},
network_approval_ceiling: None,
backend,
collaboration: Default::default(),
language: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions plugins/agent/state/src/store/stop_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ fn spec() -> (RunSpec, Vec<ContentBlob>) {
sandbox: maka_sandbox::Sandbox::Disabled,
approval: maka_sandbox::Approval::Never,
},
network_approval_ceiling: None,
backend: BackendSpec::Model {
binding: ModelBinding {
connection_id: "fixture".into(),
Expand Down
Loading