swarm: unrestricted_internet flag on the agent permissions record, unset reads as on, create writes false
This commit is contained in:
parent
e77459d2cb
commit
8699857024
2 changed files with 381 additions and 107 deletions
|
|
@ -646,21 +646,16 @@ pub(crate) async fn create_agent(
|
|||
// reads these by agent name when it first runs the agent. A failure from
|
||||
// here on puts back what was stored, so a refused creation leaves no
|
||||
// settings behind for the name.
|
||||
let stored = match &settings_queue {
|
||||
Some(client) => {
|
||||
StoredSettings::store(
|
||||
client,
|
||||
&agent,
|
||||
settings.limits.as_ref(),
|
||||
settings.inference.as_ref(),
|
||||
settings.permissions.as_ref(),
|
||||
settings.archetype.as_ref(),
|
||||
settings.agents_md.as_deref(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => StoredSettings::default(),
|
||||
};
|
||||
let stored = StoredSettings::store(
|
||||
&settings_queue,
|
||||
&agent,
|
||||
settings.limits.as_ref(),
|
||||
settings.inference.as_ref(),
|
||||
Some(&settings.permissions),
|
||||
settings.archetype.as_ref(),
|
||||
settings.agents_md.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let queued = {
|
||||
let mut sched = state
|
||||
.jobq
|
||||
|
|
@ -673,9 +668,7 @@ pub(crate) async fn create_agent(
|
|||
let ids = match queued {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
if let Some(client) = &settings_queue {
|
||||
stored.roll_back(client, &agent).await;
|
||||
}
|
||||
stored.roll_back(&settings_queue, &agent).await;
|
||||
return Err(error_problem(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&e.to_string(),
|
||||
|
|
@ -695,7 +688,7 @@ pub(crate) async fn create_agent(
|
|||
struct CreateSettings {
|
||||
limits: Option<swarm_wire::agent_limits::AgentCaps>,
|
||||
inference: Option<swarm_wire::agent_inference::AgentInference>,
|
||||
permissions: Option<swarm_wire::agent_permissions::AgentPermissions>,
|
||||
permissions: swarm_wire::agent_permissions::AgentPermissions,
|
||||
archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
|
||||
agents_md: Option<String>,
|
||||
}
|
||||
|
|
@ -709,6 +702,7 @@ impl CreateSettings {
|
|||
limits,
|
||||
inference,
|
||||
permissions,
|
||||
unrestricted_internet,
|
||||
archetype,
|
||||
agents_md,
|
||||
..
|
||||
|
|
@ -725,35 +719,24 @@ impl CreateSettings {
|
|||
Ok(Self {
|
||||
limits: limits.as_ref().map(agent_limits::into_stored).transpose()?,
|
||||
inference: inference.map(inference::into_stored).transpose()?,
|
||||
permissions: permissions.map(agent_permissions::at_create).transpose()?,
|
||||
permissions: agent_permissions::at_create(permissions, unrestricted_internet)?,
|
||||
archetype,
|
||||
agents_md,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether any setting is given.
|
||||
fn any(&self) -> bool {
|
||||
self.limits.is_some()
|
||||
|| self.inference.is_some()
|
||||
|| self.permissions.is_some()
|
||||
|| self.archetype.is_some()
|
||||
|| self.agents_md.is_some()
|
||||
}
|
||||
|
||||
/// The queue to store the settings through, `None` when none is given.
|
||||
/// An archetype is refused here unless the stored list holds it.
|
||||
/// The queue to store the settings through: the permissions are always
|
||||
/// stored, so it is always needed. An archetype is refused here unless the
|
||||
/// stored list holds it.
|
||||
async fn queue(
|
||||
&self,
|
||||
state: &AppState,
|
||||
) -> Result<Option<async_nats::Client>, problem_details::ProblemDetails> {
|
||||
if !self.any() {
|
||||
return Ok(None);
|
||||
}
|
||||
) -> Result<async_nats::Client, problem_details::ProblemDetails> {
|
||||
let client = schedules::queue(state).map_err(schedules::refused)?;
|
||||
if let Some(archetype) = &self.archetype {
|
||||
agent_archetype::check(&client, archetype).await?;
|
||||
}
|
||||
Ok(Some(client))
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1159,7 +1142,7 @@ pub(crate) async fn get_agent_icon(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::routes::tests::{SharedSched, state_with_roster};
|
||||
use crate::routes::tests::{SharedSched, state_with_roster, state_with_roster_queued};
|
||||
|
||||
/// A swarm with no identity bridge cannot answer the roster, and must
|
||||
/// say so rather than answer `[]`.
|
||||
|
|
@ -1244,6 +1227,7 @@ mod tests {
|
|||
limits: None,
|
||||
inference: None,
|
||||
permissions: None,
|
||||
unrestricted_internet: None,
|
||||
archetype: None,
|
||||
agents_md: None,
|
||||
}),
|
||||
|
|
@ -1273,7 +1257,7 @@ mod tests {
|
|||
/// could equally mean the endpoint refuses everything.
|
||||
#[tokio::test]
|
||||
async fn a_hive_in_the_roster_is_accepted_and_queues_the_graph() {
|
||||
let (state, sched) = state_with_roster();
|
||||
let (_nats, state, sched) = state_with_roster_queued().await;
|
||||
|
||||
// The response is bound rather than asserted on: node ids start at
|
||||
// zero, so every property I reached for first ("> 0") was a claim
|
||||
|
|
@ -1287,6 +1271,7 @@ mod tests {
|
|||
limits: None,
|
||||
inference: None,
|
||||
permissions: None,
|
||||
unrestricted_internet: None,
|
||||
archetype: None,
|
||||
agents_md: None,
|
||||
}),
|
||||
|
|
@ -1335,7 +1320,7 @@ mod tests {
|
|||
(r#"{"model":"-opus"}"#, "inference"),
|
||||
];
|
||||
for (value, field) in bad {
|
||||
let (state, sched) = state_with_roster();
|
||||
let (_nats, state, sched) = state_with_roster_queued().await;
|
||||
let body = format!(r#"{{"name":"atlas","hive":"pr1ma","{field}":{value}}}"#);
|
||||
let req: CreateAgentRequest = serde_json::from_str(&body).expect("body parses");
|
||||
let err = super::create_agent(axum::extract::State(state), axum::Json(req))
|
||||
|
|
@ -1492,8 +1477,11 @@ mod tests {
|
|||
.await
|
||||
.expect("a bucket that refuses any value");
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
let earlier = crate::agent_permissions::at_create(vec![hive_types::Permission::Messaging])
|
||||
.expect("valid");
|
||||
let earlier = crate::agent_permissions::at_create(
|
||||
Some(vec![hive_types::Permission::Messaging]),
|
||||
None,
|
||||
)
|
||||
.expect("valid");
|
||||
crate::agent_permissions::store(&client, "atlas", &earlier)
|
||||
.await
|
||||
.expect("an earlier grant");
|
||||
|
|
@ -1526,7 +1514,7 @@ mod tests {
|
|||
|value: Option<bytes::Bytes>| value.map(|v| String::from_utf8_lossy(&v).into_owned());
|
||||
assert_eq!(
|
||||
held(bucket.get("atlas").await.expect("read")).as_deref(),
|
||||
Some(r#"{"permissions":["messaging"]}"#),
|
||||
Some(r#"{"permissions":["messaging"],"unrestricted_internet":false}"#),
|
||||
"the earlier grant comes back"
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -1564,8 +1552,12 @@ mod tests {
|
|||
}
|
||||
|
||||
/// `state_with_roster` with a second hive, `sec0nd`.
|
||||
fn state_with_two_hives() -> (crate::routes::AppState, SharedSched) {
|
||||
let (state, sched) = state_with_roster();
|
||||
async fn state_with_two_hives() -> (
|
||||
crate::test_nats::TestNats,
|
||||
crate::routes::AppState,
|
||||
SharedSched,
|
||||
) {
|
||||
let (nats, state, sched) = state_with_roster_queued().await;
|
||||
let mut hives = (*state.hives).clone();
|
||||
hives.push(HiveEntry {
|
||||
name: "sec0nd".to_owned(),
|
||||
|
|
@ -1576,7 +1568,7 @@ mod tests {
|
|||
hives: std::sync::Arc::new(hives),
|
||||
..state
|
||||
};
|
||||
(state, sched)
|
||||
(nats, state, sched)
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
|
|
@ -1592,6 +1584,7 @@ mod tests {
|
|||
limits: None,
|
||||
inference: None,
|
||||
permissions: None,
|
||||
unrestricted_internet: None,
|
||||
archetype: None,
|
||||
agents_md: None,
|
||||
}),
|
||||
|
|
@ -1614,7 +1607,7 @@ mod tests {
|
|||
/// wanted-state read alone would miss.
|
||||
#[tokio::test]
|
||||
async fn a_name_placed_on_another_hive_is_refused_before_anything_is_queued() {
|
||||
let (state, sched) = state_with_two_hives();
|
||||
let (_nats, state, sched) = state_with_two_hives().await;
|
||||
create(&state, "atlas", "pr1ma")
|
||||
.await
|
||||
.expect("the first creation of a name must be accepted");
|
||||
|
|
@ -1643,7 +1636,7 @@ mod tests {
|
|||
/// that agent being re-created, and queues again.
|
||||
#[tokio::test]
|
||||
async fn re_creating_an_agent_on_its_own_hive_is_accepted() {
|
||||
let (state, sched) = state_with_two_hives();
|
||||
let (_nats, state, sched) = state_with_two_hives().await;
|
||||
create(&state, "atlas", "pr1ma")
|
||||
.await
|
||||
.expect("the first creation of a name must be accepted");
|
||||
|
|
@ -1659,7 +1652,7 @@ mod tests {
|
|||
/// "placed nowhere", and queues nothing.
|
||||
#[tokio::test]
|
||||
async fn an_unreadable_wanted_state_refuses_creation() {
|
||||
let (state, sched) = state_with_two_hives();
|
||||
let (_nats, state, sched) = state_with_two_hives().await;
|
||||
let state = crate::routes::AppState {
|
||||
wanted: Some(std::sync::Arc::new(wanted::WantedWriter::new(
|
||||
disconnected_client().await,
|
||||
|
|
@ -2098,7 +2091,7 @@ mod tests {
|
|||
agent: &str,
|
||||
wanted_state: swarm_wire::wanted::AgentState,
|
||||
) -> problem_details::ProblemDetails {
|
||||
let (state, _sched) = state_with_two_hives();
|
||||
let (_nats, state, _sched) = state_with_two_hives().await;
|
||||
let state = crate::routes::AppState {
|
||||
wanted: Some(std::sync::Arc::new(wanted::WantedWriter::new(
|
||||
disconnected_client().await,
|
||||
|
|
|
|||
|
|
@ -315,6 +315,21 @@ pub(crate) mod tests {
|
|||
(state, sched)
|
||||
}
|
||||
|
||||
/// [`state_with_roster`] with a connected queue, for creations: they
|
||||
/// store the agent's permissions through it. The server lives as long as
|
||||
/// the first element.
|
||||
pub(crate) async fn state_with_roster_queued()
|
||||
-> (crate::test_nats::TestNats, super::AppState, SharedSched) {
|
||||
let nats = crate::test_nats::TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let (mut state, sched) = state_with_roster();
|
||||
state.status = Some(std::sync::Arc::new(crate::status::StatusReader::new(
|
||||
client,
|
||||
std::time::Duration::from_mins(1),
|
||||
)));
|
||||
(nats, state, sched)
|
||||
}
|
||||
|
||||
/// `wanted_error_status` as a pure function first: build the exact
|
||||
/// `swarm_queue_client::Error::NotConnected` `ensure_connected` raises,
|
||||
/// wrap it the same way `?` inside `WantedWriter::view`/`set` does, and
|
||||
|
|
|
|||
Loading…
Reference in a new issue