Watch
0
0
Fork
You've already forked hyperhive
0

swarm UI: delete linked accounts — R2 fixes

Addresses argus review comment 90297 on PR #4899:

- swarm-controller/README.md: list the three DELETE routes (including
  matrix's ?revoke=true) beside the PUT/GET ones already documented.
- LinkedAccounts.tsx: a delete answering 404 means the account is
  already gone, so treat it as the delete's end state — re-fetch and
  close the dialog instead of showing an error.
- matrix_account.rs: matrix_logout treats a 401 M_UNKNOWN_TOKEN as the
  token already being revoked and proceeds with the delete; every
  other logout failure still keeps the account. Adds unit tests and
  updates docs/swarm/ui.md to match.
This commit is contained in:
atlas 2026-10-03 00:56:36 +02:00
commit b90be9e65e
4 changed files with 72 additions and 11 deletions

View file

@ -75,13 +75,16 @@ request:
swarm-controller removes every version of the entry from the swarm secret
store, and answers 404 when the store holds nothing there. It refuses `main`: the
swarm mints that account and would re-mint it. The panel requests the list
again after a delete.
again after a delete, including a 404 one: the account being already gone is
the end state the delete wanted, so the dialog closes without an error.
The matrix confirmation has a checkbox, off by default, that logs the token
out at its homeserver first (`?revoke=true`). If the homeserver doesn't
confirm the logout, or the account has no homeserver stored, the account stays
in the store and the dialog shows why. Deleting a forge account or GitHub token
leaves the token valid at its provider; revoke it there.
out at its homeserver first (`?revoke=true`). A 401 `M_UNKNOWN_TOKEN` from
that logout means the homeserver already doesn't recognise the token, so the
delete proceeds as if it had succeeded. Any other failed logout, or an account
with no homeserver stored, leaves the account in the store and the dialog
shows why. Deleting a forge account or GitHub token leaves the token valid at
its provider; revoke it there.
The agent isn't told about a delete. Its matrix daemon drops the account when
it next lists the store, within two minutes. Its forge and GitHub units never

View file

@ -96,7 +96,9 @@ export function LinkedAccounts({
deleteTarget.kind === "matrix" && revoke ? `${url}?revoke=true` : url,
{ method: "DELETE" },
);
if (!r.ok) {
// A 404 means the account the dialog is about is already gone — the
// end state the delete wanted — so it closes the same as a success.
if (!r.ok && r.status !== 404) {
setDeleteError(await readApiError(r));
return;
}

View file

@ -34,6 +34,9 @@ The swarm's control plane. The swarm UI and `swarmctl` are its clients.
`.../forge-accounts/{label}`, `.../github-account`); distinct from the agent's own swarm-minted
accounts above. `GET .../linked-accounts` lists them, plus the agent's own
`main` matrix account, by kind, name and host, never with a credential.
Each of the three also takes a `DELETE`; matrix's also takes
`?revoke=true`, which logs the stored token out at its homeserver first and
leaves the account in place if that fails.
- **Config PR status** — each agent's open config-repo PR, cached from forge
webhooks (`GET /api/config-prs`, `/api/agents/{name}/config-pr`).
- **Swarm-wide forge objects and webhooks** →

View file

@ -359,6 +359,10 @@ async fn matrix_password_login(
/// POST `<homeserver>/_matrix/client/v3/logout` with `token`, which
/// invalidates that token at the homeserver.
///
/// A 401 `M_UNKNOWN_TOKEN` means the homeserver already does not recognise
/// the token — the state a logout wants — so that one answer counts as
/// success rather than failure. Every other non-2xx is a failure.
///
/// The error names the status and the homeserver's `error` text, never the
/// token.
pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), String> {
@ -377,10 +381,17 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
if status.is_success() {
return Ok(());
}
let err = resp
.json::<serde_json::Value>()
.await
.ok()
let body = resp.json::<serde_json::Value>().await.ok();
if status == StatusCode::UNAUTHORIZED
&& body
.as_ref()
.and_then(|j| j.get("errcode"))
.and_then(serde_json::Value::as_str)
== Some("M_UNKNOWN_TOKEN")
{
return Ok(());
}
let err = body
.and_then(|j| {
j.get("error")
.and_then(serde_json::Value::as_str)
@ -394,7 +405,7 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
mod tests {
use super::{
PutMatrixAccountRequest, homeserver_or_configured_default, is_reserved_account,
password_fields, resolve_credential, token_credential,
matrix_logout, password_fields, resolve_credential, token_credential,
};
fn request(mode: &str) -> PutMatrixAccountRequest {
@ -600,4 +611,46 @@ mod tests {
"{problem:?}"
);
}
/// A stand-in homeserver on a loopback port, answering every
/// `/_matrix/client/v3/logout` with `status` and `body`.
async fn stub_homeserver(status: u16, body: &'static str) -> String {
let app = axum::Router::new().route(
"/_matrix/client/v3/logout",
axum::routing::post(move || async move {
(
axum::http::StatusCode::from_u16(status).unwrap(),
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
}),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await });
format!("http://{addr}")
}
/// A revoke whose token the homeserver already does not recognise has
/// reached logout's end state, not failed it.
#[tokio::test]
async fn logout_treats_401_m_unknown_token_as_already_revoked() {
let homeserver = stub_homeserver(
401,
r#"{"errcode":"M_UNKNOWN_TOKEN","error":"Access token invalid"}"#,
)
.await;
assert!(matrix_logout(&homeserver, "t0k3n").await.is_ok());
}
/// The control: a 401 with a different errcode is still a failure, so
/// the fold above is specific to `M_UNKNOWN_TOKEN` and not any 401.
#[tokio::test]
async fn logout_fails_on_other_401s() {
let homeserver = stub_homeserver(401, r#"{"errcode":"M_FORBIDDEN","error":"nope"}"#).await;
let err = matrix_logout(&homeserver, "t0k3n")
.await
.expect_err("not M_UNKNOWN_TOKEN");
assert!(err.contains("401"), "{err}");
}
}