hive-matrix-mcp: read the main account's token from the store too
The daemon reads each account's token from `swarm/agents/<agent>/matrix/` as the agent itself, inside its own container, and falls back to the file only when the store has none. This is #4519's read, without its `main` carve-out: the swarm now mints `main` there and no hive writes the file. The daemon unit gets the agent's store identity, spelled the way forge-token.nix spells it. A timer re-starts it while it is down: a token the swarm mints or replaces in the store changes no file, so the path watcher never fires for it, and a daemon that exited on a replaced token would otherwise stay down until the container restarts.
This commit is contained in:
parent
2776e121e5
commit
ab153bda2f
9 changed files with 670 additions and 85 deletions
|
|
@ -7,22 +7,24 @@
|
|||
//! Lifecycle:
|
||||
//! 1. Read the configured account list (`accounts::configured()` —
|
||||
//! `HIVE_MATRIX_ACCOUNTS` JSON, or the single legacy account).
|
||||
//! 2. For each account: whoami probe → recover `user_id` + `device_id`
|
||||
//! 2. For each account: resolve its access token (`account_token` — the
|
||||
//! swarm secret store first, read by this agent as itself, then the
|
||||
//! file its hive delivered) → whoami probe → recover `user_id` + `device_id`
|
||||
//! → restore matrix-sdk session (no login flow), install the
|
||||
//! message-event handler, and spawn its own sync loop.
|
||||
//! 3. Serve the MCP tools against an account→Client registry; each tool
|
||||
//! call routes to the account named in its `account` arg (the
|
||||
//! primary account when omitted).
|
||||
//!
|
||||
//! Standalone-degraded boot: the PRIMARY account having no token file →
|
||||
//! exit 0 cleanly so systemd's path-watcher restarts us once hive-c0re
|
||||
//! provisions it. A SECONDARY account missing its token is skipped (the
|
||||
//! daemon still serves the others).
|
||||
//! Standalone-degraded boot: the PRIMARY account having no token →
|
||||
//! exit 0 cleanly; systemd restarts us once one exists (the path-watcher for
|
||||
//! a token file, the store re-check timer for a stored token). A SECONDARY
|
||||
//! account missing its token is skipped (the daemon still serves the others).
|
||||
//!
|
||||
//! Stale-token recovery (`M_UNKNOWN_TOKEN`): handled in
|
||||
//! `client::build_and_restore`, and it mirrors the missing-token policy
|
||||
//! above — a rejected PRIMARY token drops the stale token + sdk state and
|
||||
//! exits 0 for systemd re-provisioning, while a rejected SECONDARY token
|
||||
//! exits 0 until a fresh token exists, while a rejected SECONDARY token
|
||||
//! is removed and that one account is skipped so the daemon keeps serving
|
||||
//! the primary and any other healthy account.
|
||||
|
||||
|
|
@ -34,7 +36,8 @@ use matrix_sdk::{Client, config::SyncSettings};
|
|||
|
||||
use hive_matrix_mcp::accounts::{AccountCfg, Registry};
|
||||
use hive_matrix_mcp::client::PermanentBringUpError;
|
||||
use hive_matrix_mcp::{accounts, client, mcp, paths, timeline, wake};
|
||||
use hive_matrix_mcp::credential::Token;
|
||||
use hive_matrix_mcp::{accounts, client, credential, mcp, paths, timeline, wake};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hive-matrix-daemon", about = "matrix-sdk client + MCP daemon")]
|
||||
|
|
@ -97,13 +100,13 @@ async fn main() -> Result<()> {
|
|||
sync_loops.push(sync_loop);
|
||||
}
|
||||
// No token yet: for the primary that means the daemon isn't
|
||||
// useful — exit 0 like the legacy single-account path so the
|
||||
// systemd path-watcher restarts us when the token appears.
|
||||
// useful — exit 0, and systemd restarts us once a token exists
|
||||
// (the path-watcher for a file, the re-check timer for the store).
|
||||
Ok(None) if is_primary => {
|
||||
tracing::warn!(
|
||||
account = %cfg.name,
|
||||
"primary matrix account has no token yet; exiting cleanly \
|
||||
(systemd restarts us when hive-c0re provisions it)"
|
||||
(systemd restarts us once one exists)"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -259,6 +262,46 @@ async fn bring_up_secondary_with_retry(
|
|||
None
|
||||
}
|
||||
|
||||
/// Resolve `cfg`'s access token, preferring the copy this agent can fetch
|
||||
/// itself.
|
||||
///
|
||||
/// 🏛️ The store read is the point: the credential at
|
||||
/// `swarm/agents/<agent>/matrix/<account>` is **this agent's**, and it is read
|
||||
/// from inside this agent's container under this agent's own certificate,
|
||||
/// never by the hive on the agent's behalf. It is held in memory from here to
|
||||
/// `restore_session` and is written nowhere.
|
||||
///
|
||||
/// `main` included: `swarm-controller` mints it with the swarm's appservice
|
||||
/// token and stores it at `swarm/agents/<agent>/matrix/main`, and no hive
|
||||
/// mints it any more. An agent whose harness forwards no
|
||||
/// [`credential::ENV_AGENT`] cannot name its own subtree, so it reads the file.
|
||||
///
|
||||
/// The file is what remains when the store answers nothing: an extra account
|
||||
/// the hive-side delivery loop still writes, or a `main` token a hive minted
|
||||
/// before the swarm did. A store read that *fails* falls back too, and says
|
||||
/// why.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`credential::Token::from_file`]: a token file that exists but cannot be
|
||||
/// read, or is empty.
|
||||
async fn account_token(cfg: &AccountCfg) -> Result<Option<Token>> {
|
||||
if let Some(agent) = credential::agent_name() {
|
||||
match Token::from_store(&agent, &cfg.name).await {
|
||||
Ok(Some(token)) => return Ok(Some(token)),
|
||||
// No store in this deployment: nothing to report, the hive-side
|
||||
// delivery is the whole mechanism here.
|
||||
Ok(None) => {}
|
||||
Err(e) => tracing::warn!(
|
||||
account = %cfg.name,
|
||||
error = %format!("{e:#}"),
|
||||
"could not read this account's credential from the store as this agent; \
|
||||
falling back to the token the hive delivered"
|
||||
),
|
||||
}
|
||||
}
|
||||
Token::from_file(&cfg.token_file).await
|
||||
}
|
||||
|
||||
/// Restore one account's client (when its token exists), install its
|
||||
/// message handler, and build its sync loop. Returns
|
||||
/// `Ok(Some((client, sync_loop)))` when the account came up, `Ok(None)`
|
||||
|
|
@ -280,23 +323,19 @@ async fn bring_up_account(
|
|||
);
|
||||
return Ok(None);
|
||||
};
|
||||
if !tokio::fs::try_exists(&cfg.token_file)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let Some(token) = account_token(cfg).await? else {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
tracing::info!(
|
||||
account = %cfg.name,
|
||||
homeserver,
|
||||
token_file = %cfg.token_file.display(),
|
||||
credential = %token.origin(),
|
||||
state_dir = %cfg.state_dir.display(),
|
||||
"bringing up matrix account"
|
||||
);
|
||||
let client =
|
||||
client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary)
|
||||
.await
|
||||
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
|
||||
let client = client::build_and_restore(&homeserver, &token, &cfg.state_dir, is_primary)
|
||||
.await
|
||||
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
|
||||
// Best-effort: sync the agent icon to this account's matrix avatar over
|
||||
// the live (authenticated, correct-homeserver) Client. Replaces the old
|
||||
// curl oneshot; failures are swallowed inside sync_avatar.
|
||||
|
|
|
|||
Loading…
Reference in a new issue