destroying an agent clears its per-agent archetype-flow switch-offs
This commit is contained in:
parent
13ff33ead0
commit
d8703f40dc
3 changed files with 117 additions and 0 deletions
|
|
@ -162,6 +162,10 @@ within about 10 seconds, and the card has no `edit` button.
|
|||
just one, and to nobody until you name an agent when it has more. A custom
|
||||
flow can also request a review, which an archetype's flow can't. Switch the
|
||||
archetype's flow off to replace it with your copy.
|
||||
- Destroying an agent removes its per-agent switch-offs from the list, so
|
||||
a new agent of the same name starts with every archetype flow on. If that
|
||||
cleanup fails, the controller logs the agent and leaves the keys; destroying
|
||||
the agent again retries it.
|
||||
- A key for a flow or agent that no longer exists stays in the list and does nothing, so
|
||||
editing an archetype never breaks the stored flows. The key stays
|
||||
after its flow goes: a later flow with the same archetype and id comes up
|
||||
|
|
|
|||
|
|
@ -229,6 +229,80 @@ async fn archetype_flows(client: &async_nats::Client) -> Vec<ArchetypeFlowView>
|
|||
}
|
||||
}
|
||||
|
||||
/// `disabled` less the switch-offs for `agent` alone (`archetype:…/…@agent`; no
|
||||
/// built-in id has an `@`),
|
||||
/// or `None` when it has none. The per-archetype keys stay.
|
||||
fn without_agent_keys(disabled: &[String], agent: &str) -> Option<Vec<String>> {
|
||||
let suffix = format!("@{agent}");
|
||||
let kept: Vec<String> = disabled
|
||||
.iter()
|
||||
.filter(|k| !k.ends_with(&suffix))
|
||||
.cloned()
|
||||
.collect();
|
||||
(kept.len() != disabled.len()).then_some(kept)
|
||||
}
|
||||
|
||||
/// Remove the keys that switch archetype flows off for `agent` from the stored
|
||||
/// document, in the compare-and-set loop a `PUT` uses, so a concurrent edit
|
||||
/// is not lost. Answers the keys removed. No write when there are none.
|
||||
async fn clear_agent_keys(client: &async_nats::Client, agent: &str) -> anyhow::Result<Vec<String>> {
|
||||
let store = open(client).await?;
|
||||
for _ in 0..MAX_ATTEMPTS {
|
||||
let (current, revision) = stored(&store).await?;
|
||||
let Stored::Document(mut doc) = current else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(kept) = without_agent_keys(&doc.disabled_builtins, agent) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let removed: Vec<String> = doc
|
||||
.disabled_builtins
|
||||
.iter()
|
||||
.filter(|k| !kept.contains(k))
|
||||
.cloned()
|
||||
.collect();
|
||||
doc.disabled_builtins = kept;
|
||||
if write_revision(&store, notify_flows::KEY, &doc, revision)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(removed);
|
||||
}
|
||||
}
|
||||
anyhow::bail!("gave up after {MAX_ATTEMPTS} conflicting writes")
|
||||
}
|
||||
|
||||
/// Clear a destroyed agent's per-agent switch-offs, so a new agent of that
|
||||
/// name starts with every archetype flow on. A failure is logged with the keys
|
||||
/// left behind and does not stop the destroy.
|
||||
pub(crate) async fn clear_on_destroy(state: &AppState, hive: &str, agent: &str) {
|
||||
let cleared = async {
|
||||
let client = queue(state).map_err(|(_, why)| anyhow::anyhow!(why))?;
|
||||
clear_agent_keys(&client, agent).await
|
||||
}
|
||||
.await;
|
||||
match cleared {
|
||||
Ok(removed) if removed.is_empty() => {}
|
||||
Ok(removed) => {
|
||||
tracing::info!(
|
||||
hive,
|
||||
agent,
|
||||
?removed,
|
||||
"destroyed agent's flow switch-offs cleared"
|
||||
);
|
||||
}
|
||||
Err(e) => tracing::error!(
|
||||
hive,
|
||||
agent,
|
||||
error = %format!("{e:#}"),
|
||||
"clearing this agent's flow switch-offs (`archetype:…@<agent>` in the flows \
|
||||
document's disabled list) FAILED; it was destroyed anyway, so a new agent of the \
|
||||
same name starts with those flows off. Declaring the agent destroyed again \
|
||||
re-runs this."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unreadable(why: &DecodeError) -> ProblemDetails {
|
||||
error_problem(
|
||||
StatusCode::CONFLICT,
|
||||
|
|
@ -653,4 +727,42 @@ mod tests {
|
|||
assert_eq!(refused.status, Some(StatusCode::BAD_REQUEST), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroying an agent removes its own switch-offs from the document and
|
||||
/// nothing else; a document without any is not written.
|
||||
#[tokio::test]
|
||||
async fn a_destroyed_agents_switch_offs_are_cleared_and_nothing_else() {
|
||||
let nats = TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
|
||||
let keys = [
|
||||
"archetype:a/f",
|
||||
"archetype:a/f@alice",
|
||||
"archetype:a/f@bob",
|
||||
"archetype:a/g@alice",
|
||||
"closed",
|
||||
];
|
||||
let Json(saved) = put_flows(State(state.clone()), Ok(body(0, &keys, vec![])))
|
||||
.await
|
||||
.expect("put");
|
||||
|
||||
let removed = clear_agent_keys(&client, "alice").await.expect("clear");
|
||||
assert_eq!(removed, ["archetype:a/f@alice", "archetype:a/g@alice"]);
|
||||
let Json(after) = get_flows(State(state.clone())).await.expect("get");
|
||||
assert_eq!(
|
||||
after.disabled_builtins,
|
||||
["archetype:a/f", "archetype:a/f@bob", "closed"]
|
||||
);
|
||||
assert!(after.revision > saved.revision);
|
||||
|
||||
// Nothing of alice's left: no write.
|
||||
assert!(clear_agent_keys(&client, "alice").await.unwrap().is_empty());
|
||||
let Json(same) = get_flows(State(state.clone())).await.expect("get");
|
||||
assert_eq!(same.revision, after.revision);
|
||||
// A name that is the end of another's is not it.
|
||||
assert!(clear_agent_keys(&client, "ob").await.unwrap().is_empty());
|
||||
let Json(same) = get_flows(State(state)).await.expect("get");
|
||||
assert_eq!(same.disabled_builtins, after.disabled_builtins);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ pub(crate) async fn set_agent_state(
|
|||
revoke_or_complain(&hive, &agent).await;
|
||||
agent_permissions::clear_on_destroy(&state, &hive, &agent).await;
|
||||
agent_archetype::clear_on_destroy(&state, &hive, &agent).await;
|
||||
crate::notify_flows::clear_on_destroy(&state, &hive, &agent).await;
|
||||
agents_md_initial::clear_on_destroy(&state, &hive, &agent).await;
|
||||
egress_allow::clear_on_destroy(&state, &hive, &agent).await;
|
||||
egress_deny::clear_on_destroy(&state, &hive, &agent).await;
|
||||
|
|
|
|||
Loading…
Reference in a new issue