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 build_logs;
|
||||||
mod journal;
|
mod journal;
|
||||||
mod lifecycle_ops;
|
mod lifecycle_ops;
|
||||||
|
mod matrix_accounts;
|
||||||
pub(crate) mod permissions;
|
pub(crate) mod permissions;
|
||||||
mod questions;
|
mod questions;
|
||||||
mod reminders;
|
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/journal-host", get(journal::get_journal_host))
|
||||||
.route("/api/approval-diff/{id}", get(approvals::get_approval_diff))
|
.route("/api/approval-diff/{id}", get(approvals::get_approval_diff))
|
||||||
.route("/api/state-file", get(state_files::get_state_file))
|
.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/reminders", get(reminders::api_reminders))
|
||||||
.route("/api/operator-inbox", get(api_operator_inbox))
|
.route("/api/operator-inbox", get(api_operator_inbox))
|
||||||
.route("/api/stats-hive", get(api_stats_hive))
|
.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