dashboard: GET /api/matrix-accounts — list an agent's provisioned matrix accounts (#1698)
This commit is contained in:
parent
06d57a61f8
commit
1ebc384e66
2 changed files with 133 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ mod approvals;
|
|||
mod build_logs;
|
||||
mod journal;
|
||||
mod lifecycle_ops;
|
||||
mod matrix_accounts;
|
||||
pub(crate) mod permissions;
|
||||
mod questions;
|
||||
mod reminders;
|
||||
|
|
@ -100,6 +101,10 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/api/journal-host", get(journal::get_journal_host))
|
||||
.route("/api/approval-diff/{id}", get(approvals::get_approval_diff))
|
||||
.route("/api/state-file", get(state_files::get_state_file))
|
||||
.route(
|
||||
"/api/matrix-accounts",
|
||||
get(matrix_accounts::get_matrix_accounts),
|
||||
)
|
||||
.route("/api/reminders", get(reminders::api_reminders))
|
||||
.route("/api/operator-inbox", get(api_operator_inbox))
|
||||
.route("/api/stats-hive", get(api_stats_hive))
|
||||
|
|
|
|||
128
hive-c0re/src/dashboard/matrix_accounts.rs
Normal file
128
hive-c0re/src/dashboard/matrix_accounts.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Matrix-account listing for the dashboard (#1698 backend, BE-1).
|
||||
//!
|
||||
//! `GET /api/matrix-accounts?agent=<name>` returns the matrix accounts an
|
||||
//! agent currently has a token provisioned for. v1 derives the list cheaply
|
||||
//! from the agent's state dir — every `matrix-token*` file is a provisioned
|
||||
//! account (`matrix-token` = the hive-internal `main` account;
|
||||
//! `matrix-token-<name>` = an extra account, matching the daemon's
|
||||
//! path-watcher glob). It does not read the nix config, so it lists what is
|
||||
//! *provisioned*, not the full configured set: a config-declared account
|
||||
//! without a token yet appears once it is provisioned via the login form.
|
||||
//! Homeserver + live up/down status arrive in v2 from the daemon's account
|
||||
//! registry; here `homeserver` is always `null`.
|
||||
|
||||
use axum::extract::Query;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::error_response;
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct MatrixAccountsQuery {
|
||||
agent: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MatrixAccount {
|
||||
name: String,
|
||||
/// Always `None` in v1 (derived from the token filename, which carries no
|
||||
/// homeserver); populated in v2 from the daemon's account registry.
|
||||
homeserver: Option<String>,
|
||||
token_present: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MatrixAccountsResponse {
|
||||
accounts: Vec<MatrixAccount>,
|
||||
}
|
||||
|
||||
/// Map a state-dir filename to the matrix account name it provisions, or
|
||||
/// `None` if it is not a token file. `matrix-token` → the hive-internal
|
||||
/// `main` account; `matrix-token-<name>` → the extra account `<name>`.
|
||||
fn account_name_from_filename(fname: &str) -> Option<String> {
|
||||
if fname == "matrix-token" {
|
||||
return Some("main".to_owned());
|
||||
}
|
||||
let suffix = fname.strip_prefix("matrix-token-")?;
|
||||
if suffix.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(suffix.to_owned())
|
||||
}
|
||||
|
||||
pub(super) async fn get_matrix_accounts(Query(q): Query<MatrixAccountsQuery>) -> Response {
|
||||
let agent = q.agent.trim();
|
||||
// Agent names are simple identifiers; reject anything else so a crafted
|
||||
// `agent` can't escape the per-agent state root via path components.
|
||||
if agent.is_empty()
|
||||
|| !agent
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return error_response(&format!("matrix-accounts: invalid agent name {agent:?}"));
|
||||
}
|
||||
|
||||
let dir = Coordinator::agent_notes_dir(agent);
|
||||
let mut accounts = Vec::new();
|
||||
match std::fs::read_dir(&dir) {
|
||||
Ok(entries) => {
|
||||
for entry in entries.flatten() {
|
||||
if !entry.file_type().is_ok_and(|ft| ft.is_file()) {
|
||||
continue;
|
||||
}
|
||||
let fname = entry.file_name();
|
||||
let Some(fname) = fname.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(name) = account_name_from_filename(fname) {
|
||||
accounts.push(MatrixAccount {
|
||||
name,
|
||||
homeserver: None,
|
||||
token_present: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// No state dir / no tokens yet is a normal empty result, not an error.
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => {
|
||||
return error_response(&format!("matrix-accounts: read {}: {e}", dir.display()));
|
||||
}
|
||||
}
|
||||
accounts.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
axum::Json(MatrixAccountsResponse { accounts }).into_response()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::account_name_from_filename;
|
||||
|
||||
#[test]
|
||||
fn main_account_from_bare_token() {
|
||||
assert_eq!(
|
||||
account_name_from_filename("matrix-token").as_deref(),
|
||||
Some("main")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_account_from_suffixed_token() {
|
||||
assert_eq!(
|
||||
account_name_from_filename("matrix-token-catgirl").as_deref(),
|
||||
Some("catgirl")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_suffix_is_not_an_account() {
|
||||
assert_eq!(account_name_from_filename("matrix-token-"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_token_files_ignored() {
|
||||
assert_eq!(account_name_from_filename("matrix-sdk-state"), None);
|
||||
assert_eq!(account_name_from_filename("notes.md"), None);
|
||||
assert_eq!(account_name_from_filename("matrix-avatar-icon-hash"), None);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue