hive-c0re: stop minting agents' matrix accounts

The swarm mints each agent's `main` account now, so the hive's own mint
goes: `ensure_user_for`, `finish_user_provisioning`, `sync_agent`,
`sync_agent_standalone`, `token_path`, `legacy_password_path`,
`auto_reset_password` and `token_file_present`, and the calls from the startup sweep and the
rebuild bookkeeping. Both mints pinned the device `hyperhive-<agent>`, so
leaving this one would have each re-login kill the other's token.

`hivectl matrix create-user` refuses an agent's name and says where its
account comes from. Everything that still uses the hive's appservice token
stays: the hive's own account, the Space and chat room, and operator
accounts.
This commit is contained in:
atlas 2026-09-25 02:08:23 +02:00 • committed by mara
commit 89a5dd752c
6 changed files with 61 additions and 351 deletions

View file

@ -142,7 +142,7 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for
###### **Subcommands:**
* `create-user` — Create or refresh the matrix account + access token for `<name>`
* `create-user` — Create a matrix account for a person or other non-agent `<name>` and print its access token to stdout
* `sync-admin` — Provision (or re-provision) the matrix appservice's sender account
* `promote-user` — Promote a matrix user to homeserver admin
* `reset-password` — Reset a matrix user's password via the admin API
@ -152,15 +152,15 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for
## `hivectl matrix create-user`
Create or refresh the matrix account + access token for `<name>`.
Create a matrix account for a person or other non-agent `<name>` and print its access token to stdout.
For an existing agent, persists the token to its state dir; for a human/other account, prints the access token to stdout. Set a password to enable matrix web-client login (otherwise it uses a random throwaway).
Refuses an agent's name: its account comes from the swarm (`swarm-controller` creates it and stores its token where the agent reads it). Set a password to enable matrix web-client login (otherwise it uses a random throwaway).
**Usage:** `hivectl matrix create-user [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Matrix localpart. For agents: the container/agent name. For humans: any matrix localpart — `mara`, `damocles`, etc
* `<NAME>` — Matrix localpart of a non-agent account — `mara`, `damocles`, etc
###### **Options:**

View file

@ -50,7 +50,6 @@ Manual entry to the same idempotent matrix provisioning flow
running (`services.hyperhive.deploy.matrix.enable = true`).
```bash
hivectl matrix create-user iris # provision (or re-provision) matrix account for agent `iris`
hivectl matrix create-user mara # create matrix account for a human; prints access_token to stdout
hivectl matrix create-user mara --password hunter2 # set a client-login password
hivectl matrix sync-admin # provision / refresh the appservice's sender account
@ -60,9 +59,9 @@ hivectl matrix invite mara # invite a user to the hive Space
hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a specific room/alias
```
- `create-user`: for agents, persists the `access_token` to
`<state>/matrix-token`. Skips registration when the file already
exists — delete it first to force re-registration.
- `create-user`: for people and other non-agent accounts. It refuses
an agent's name: `swarm-controller` creates an agent's account and
stores its token where the agent's daemon reads it.
- `sync-admin`: ensures this hive's appservice sender account
(`@hive-<hive>:<server_name>`, one per hive) exists
(the account `hive-c0re` provisions rooms with). Token persisted to the

View file

@ -465,7 +465,7 @@ async fn run_swap(coord: &Arc<Coordinator>, name: &str, id: NodeId) -> Result<()
/// The post-`Swap` bookkeeping tail, split into its own node for dashboard
/// visibility + retry granularity. Deps `AfterOk(Swap)`, so reaching here
/// means the profile swap succeeded. Store/forge/matrix work only — no nix
/// means the profile swap succeeded. Store/forge work only — no nix
/// build (build-slot-exempt); the agent lease taken at `Swap` is still held
/// (the whole chain up to `Reconcile` is one agent's subgraph).
async fn run_rebuild_bookkeeping(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
@ -477,11 +477,11 @@ async fn run_rebuild_bookkeeping(coord: &Arc<Coordinator>, name: &str) -> Result
// The `Rebuilt` manager event is emitted exactly once per agent by the DAG's
// `EmitRebuilt` tail — emitting ok here and letting a failed tail `Reconcile`
// add a contradictory !ok would double-report the same rebuild.
// Full forge + matrix sync on every successful rebuild so the rebuild
// path is equivalent to the startup sweep: tokens, config-repo mirror,
// meta access all recover without a hive-c0re restart.
// Full forge sync on every successful rebuild so the rebuild path is
// equivalent to the startup sweep: tokens, config-repo mirror, meta
// access all recover without a hive-c0re restart. (No matrix step: the
// swarm mints an agent's matrix account, not this hive.)
crate::forge::sync_agent(name, crate::forge::core_token().as_deref()).await;
crate::matrix::sync_agent_standalone(name).await;
// Wake the agent on its next turn so claude sees a "you were rebuilt"
// hint; rescan so dashboards drop the "needs update" chip; lock bump →
// meta-inputs re-render.

View file

@ -20,8 +20,6 @@ use std::path::PathBuf;
use anyhow::{Context, Result};
use reqwest::StatusCode;
use crate::coordinator::Coordinator;
/// Client-server API base this daemon provisions against, from
/// `HIVE_MATRIX_API_URL` (set by the hyperhive NixOS module from
/// `services.hyperhive.swarm.matrix.apiUrl`).
@ -126,45 +124,15 @@ pub fn sender_token_path() -> PathBuf {
crate::paths::matrix_sender_token()
}
/// Token file inside the agent's bind-mounted state dir (visible as
/// `/state/matrix-token` from inside the container).
fn token_path(name: &hive_types::Ident) -> PathBuf {
Coordinator::agent_notes_dir(name).join("matrix-token")
}
/// Whether an agent's token file is present: a non-empty regular file.
/// Decided from metadata alone, because hive-priv writes the file 0600
/// and owned by the agent, so `hive-core` cannot read it but can stat it
/// through the 0755 state dir. A check that reads the file always
/// fails here and makes every sweep re-mint the token.
fn token_file_present(path: &std::path::Path) -> bool {
std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() > 0)
}
/// Password file for the agent's matrix account. Stored OUTSIDE the
/// purgeable `agent_state_root` tree so it survives `destroy --purge`
/// and allows re-login recovery when the same agent name is re-spawned.
/// Password file for a matrix account this hive holds a password for: its own
/// `@hive-<hive>:` account, or one reset through the admin room. Stored OUTSIDE
/// the purgeable `agent_state_root` tree so it survives `destroy --purge`.
///
/// Path: `/var/lib/hyperhive/matrix/creds/<name>-password`
///
/// The token file lives inside the agent's bind-mounted state dir (under
/// `agent_notes_dir`) so the agent container can read it; the password
/// file is host-side only (agents never log in by password — they use
/// the access token exclusively) and belongs with other hive-c0re
/// credential state, not inside the purgeable per-agent tree.
fn password_path(name: &str) -> PathBuf {
crate::paths::matrix_creds_dir().join(format!("{name}-password"))
}
/// Legacy password path (inside the old purgeable `agent_notes_dir`).
/// Used only during the one-time migration in [`ensure_user_for`] to
/// move credentials from old deployments to the new location. Safe to
/// call after `destroy --purge` — the path will simply not exist and
/// the migration is a no-op.
fn legacy_password_path(name: &hive_types::Ident) -> PathBuf {
Coordinator::agent_notes_dir(name).join("matrix-password")
}
/// Host path where the hive Matrix Space room ID is persisted.
/// Outside every purgeable path — not deleted by `destroy --purge`.
#[must_use]
@ -319,8 +287,8 @@ async fn register_user(
"type": "m.login.application_service",
"username": localpart,
"password": password,
// device_id stays stable across re-runs of ensure_user_for so
// a re-mint doesn't strand orphan devices in tuwunel.
// device_id stays stable across re-runs so a re-mint doesn't
// strand orphan devices in tuwunel.
"device_id": format!("hyperhive-{agent}"),
"initial_device_display_name": format!("hyperhive ({agent})"),
"inhibit_login": false,
@ -416,32 +384,6 @@ async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Re
extract_access_token(&json)
}
/// Auto-recovery helper: reset a user's matrix password through the admin
/// room when the locally stored password is missing. Returns the new
/// password (already persisted to [`password_path`]) on success.
///
/// ⚠️ Needs an **admin sender**, which `@hive-<hive>:` is not — the reset is a
/// `!admin` command and tuwunel only treats a message as a command when
/// its sender is an admin in that room. So this recovery path fails until
/// the two admin operations are rehomed at swarm level; the ordinary
/// route (the stored password, or an appservice login) is unaffected.
///
/// Called by [`ensure_user_for`] when registration returns `M_USER_IN_USE`
/// but the password file is absent — covers the case where agent state dirs
/// were wiped but the homeserver still has the accounts.
async fn auto_reset_password(client: &reqwest::Client, name: &str) -> anyhow::Result<String> {
let sender_token = read_sender_token()
.context("matrix: the matrix sender token is unavailable for auto-recovery")?;
let server_name = discover_server_name(client)
.await
.context("matrix: discover_server_name for auto-recovery")?;
let effective_password = reset_user_password(client, &sender_token, name, &server_name)
.await
.with_context(|| format!("matrix: admin-room password reset for {name} (auto-recovery)"))?;
tracing::info!(%name, "matrix: auto-recovered password via admin-room reset");
Ok(effective_password)
}
// ---------------------------------------------------------------------------
// Admin-room fallback for password reset
// ---------------------------------------------------------------------------
@ -711,162 +653,13 @@ async fn admin_room_reset_password(
})
}
/// Ensure `name` has a matrix user + token file on the local
/// homeserver. Skips provisioning entirely if the token file already
/// exists (treating a present token as proof the account is good).
/// To force re-provisioning, delete the token file.
///
/// When registration fails with `M_USER_IN_USE` (account exists in the
/// homeserver but the token file was deleted) this falls back to
/// `m.login.password` using the persisted `matrix-password` file. If
/// that file is also missing, recovery requires manual intervention:
/// `hivectl matrix create-user <name> --password <pw>`.
///
/// `client` is shared across the sweep so we build one reqwest
/// connection pool for all agents rather than one per call.
pub async fn ensure_user_for(client: &reqwest::Client, name: &str, as_token: &str) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let agent = hive_types::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
if token_file_present(&token_path(&agent)) {
tracing::debug!(%name, "matrix: token already present");
return Ok(());
}
// One-time migration: move the password from the old location inside
// agent_notes_dir (purgeable) to the new location outside it.
let new_pw_path = password_path(name);
let old_pw_path = legacy_password_path(&agent);
if !new_pw_path.exists() && old_pw_path.exists() {
if let Some(parent) = new_pw_path.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Err(e) = std::fs::rename(&old_pw_path, &new_pw_path) {
// Rename across filesystems or read-only src — copy + delete.
if let Ok(content) = std::fs::read(&old_pw_path) {
if std::fs::write(&new_pw_path, &content).is_ok() {
let _ = std::fs::remove_file(&old_pw_path);
tracing::info!(%name, "matrix: migrated password file to non-purgeable location");
}
} else {
tracing::warn!(%name, rename_error = ?e, "matrix: password migration failed — could not read old path (old path stays)");
}
} else {
tracing::info!(%name, "matrix: migrated password file to non-purgeable location");
}
}
let password = random_password()?;
let access_token = match register_user(client, name, as_token, &password).await {
Ok(token) => {
// Successful registration — persist the password so we can
// fall back to login if the token file is deleted later.
let pw_path = password_path(name);
if let Some(parent) = pw_path.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Err(e) = std::fs::write(&pw_path, format!("{password}\n")) {
tracing::warn!(%name, error = ?e, "matrix: failed to persist password (token still saved)");
} else {
let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600));
}
token
}
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
// Account already exists and its token file was lost. The
// appservice can mint a session for any account in its
// namespace, so this needs no password and no admin rights —
// try it before the password paths below, which exist for
// accounts created before the appservice did (or named
// outside its namespace) and stay as the fallback.
tracing::info!(%name, "matrix: user already exists, logging in as the appservice");
match appservice_login(client, as_token, name).await {
Ok(token) => return finish_user_provisioning(name, &token).await,
Err(e) => tracing::warn!(
%name,
error = ?e,
"matrix: appservice login failed; falling back to the stored password"
),
}
let pw_path = password_path(name);
let stored = if let Some(pw) = std::fs::read_to_string(&pw_path)
.ok()
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
{
pw
} else {
// Password file missing — attempt auto-recovery through the
// admin room.
// This covers the case where agent state dirs were wiped but the
// homeserver still has the accounts. Requires the hive's sender
// token at /var/lib/hyperhive/matrix/access-token, and an admin
// sender, which `@hive-<hive>:` is not.
tracing::info!(
%name,
"matrix: stored password missing, attempting admin-room auto-recovery"
);
match auto_reset_password(client, name).await {
Ok(new_pw) => new_pw,
Err(e) => {
anyhow::bail!(
"matrix: user {name} already exists but password is missing \
and admin auto-recovery failed ({e:#}) — run:\n\
hivectl matrix reset-password {name}\n\
hivectl matrix create-user {name}"
)
}
}
};
login_user(client, name, &stored).await.with_context(|| {
format!(
"matrix: login fallback for {name} failed — if homeserver was wiped, delete \
the matrix-password file and retry"
)
})?
}
Err(other) => return Err(other),
};
finish_user_provisioning(name, &access_token).await
}
/// Persist a freshly-obtained agent access token and kick the agent's
/// matrix daemon. Shared by every way [`ensure_user_for`] can end up
/// holding a token — creation, appservice login, password login — so a
/// new recovery path cannot forget half of it.
async fn finish_user_provisioning(name: &str, access_token: &str) -> Result<()> {
// Write the token via hive-priv (root helper): hive-c0re runs as the
// unprivileged `hive-core` user and cannot write to agent-owned state
// directories directly. hive-priv writes the file 0600 and chowns it
// to the agent user so it is readable from inside the container.
crate::priv_client::write_agent_matrix_token(name, access_token, None, None)
.await
.with_context(|| format!("matrix: write matrix-token for {name} via hive-priv"))?;
tracing::info!(%name, "matrix: provisioned access token");
// Kick the daemon so it picks up the new token without waiting for a
// full container restart — see docs/integrations/matrix.md::Provisioning flow.
if let Err(e) = crate::priv_client::restart_matrix_daemon(name).await {
tracing::warn!(%name, error = ?e, "matrix: could not restart hive-matrix-daemon (token written; daemon will reload on next container start)");
} else {
tracing::info!(%name, "matrix: restarted hive-matrix-daemon to pick up new token");
}
Ok(())
}
/// Register a matrix account for `name` with the supplied `password`
/// and return the freshly-minted access token. Unlike [`ensure_user_for`],
/// the token is **not** persisted to disk — the caller is responsible
/// for storing it. Used by `hivectl matrix create-user` for human
/// (non-agent) accounts so we don't create stray
/// `/var/lib/hyperhive/agents/<name>/` directories for users that
/// aren't agents. For operator accounts the caller passes a real
/// and return the freshly-minted access token. The token is **not**
/// persisted to disk — the caller is responsible for storing it. Used by
/// `hivectl matrix create-user` for human (non-agent) accounts. For operator accounts the caller passes a real
/// password so the operator can `m.login.password` into matrix web
/// clients afterwards; for headless agent re-provisioning the caller
/// can pass [`random_password`] to keep the existing throwaway
/// behaviour.
/// clients afterwards; without one the caller passes
/// [`random_password`].
///
/// **Not idempotent**: the matrix `/register` endpoint returns
/// `M_USER_IN_USE` (HTTP 400) on a second call for the same localpart,
@ -882,44 +675,6 @@ pub async fn provision_user_token(
register_user(client, name, as_token, password).await
}
/// Per-agent matrix sync: ensure the agent has a matrix account + token.
/// All operations are idempotent; failures are logged as warnings but
/// don't abort the caller.
pub async fn sync_agent(client: &reqwest::Client, name: &str, as_token: &str) {
if let Err(e) = ensure_user_for(client, name, as_token).await {
tracing::warn!(%name, error = ?e, "matrix: ensure_user failed");
}
}
/// Standalone per-agent sync that handles its own setup: checks if
/// hive-matrix is present, reads the appservice token, and builds
/// an HTTP client before delegating to [`sync_agent`]. Mirrors the
/// setup in [`ensure_all`] so the rebuild path and the startup sweep
/// stay equivalent. No-op when the matrix container is absent.
pub async fn sync_agent_standalone(name: &str) {
if !is_present() {
return;
}
let as_token = match read_appservice_token() {
Ok(t) => t,
Err(e) => {
tracing::warn!(%name, error = ?e, "matrix: read_appservice_token failed");
return;
}
};
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
.build()
{
Ok(c) => c,
Err(e) => {
tracing::warn!(%name, error = ?e, "matrix: build HTTP client failed");
return;
}
};
sync_agent(&client, name, &as_token).await;
}
/// Ensure the hive's `@hive-<hive>:` matrix user exists and that its access token is
/// persisted at [`sender_token_path()`].
///
@ -1179,8 +934,7 @@ pub async fn promote_user_to_admin(
///
/// Sends `!admin users reset-password @<localpart>:<server>` to the admin room as
/// `@hive-<hive>:`, polls for the bot's response containing the new password, and persists
/// it to the non-purgeable creds path so [`ensure_user_for`] can re-login
/// on the next provisioning sweep.
/// it to the non-purgeable creds path.
///
/// ⚠️ Same admin-**sender** requirement as [`promote_user_to_admin`], and
/// the same consequence: `@hive-<hive>:` is an ordinary account with no admin
@ -1874,10 +1628,11 @@ async fn resolve_room_alias(
.ok_or_else(|| anyhow::anyhow!("matrix: alias {alias} response missing room_id: {json}"))
}
/// Sweep every existing container (manager + sub-agents) and ensure
/// each has a matrix user + token on the local homeserver. Called at
/// hive-c0re startup, alongside `forge::ensure_all`, and then
/// periodically (see the caller in `main.rs`). No-op when the
/// Make sure the hive's own `@hive-<hive>:` account exists, then provision
/// the hive Space and chat room and invite every agent container to both.
/// Agents' own accounts are not created here: `swarm-controller` mints them
/// with the swarm's appservice token. Called at hive-c0re startup, alongside
/// `forge::ensure_all`, and then periodically (see the caller in `main.rs`). No-op when the
/// hive-matrix container isn't running. Per-step failures are logged
/// but don't abort the sweep.
///
@ -1893,9 +1648,7 @@ pub async fn ensure_all() -> bool {
}
let mut ok = true;
// Loud and non-destructive: with no appservice token this sweep can
// create nothing, so it does nothing. Agents that already hold a
// token keep using it — their accounts and sessions are untouched by
// anything in here.
// create nothing, so it does nothing.
let as_token = match read_appservice_token() {
Ok(t) => t,
Err(e) => {
@ -1903,8 +1656,7 @@ pub async fn ensure_all() -> bool {
return false;
}
};
// One HTTP client for the whole sweep — connection pool is
// reused across agents.
// One HTTP client for the whole sweep.
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
.build()
@ -1932,7 +1684,6 @@ pub async fn ensure_all() -> bool {
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
continue;
};
sync_agent(&client, name, &as_token).await;
agent_names.push(name.to_owned());
}
@ -2101,30 +1852,4 @@ mod tests {
let err = extract_access_token(&body).unwrap_err();
assert!(err.to_string().contains("missing access_token"));
}
#[test]
fn token_file_present_only_for_a_non_empty_regular_file() {
let dir = tempfile::tempdir().expect("tempdir");
let token = dir.path().join("matrix-token");
assert!(!token_file_present(&token), "missing file");
std::fs::write(&token, "").unwrap();
assert!(!token_file_present(&token), "empty file");
std::fs::write(&token, "tok\n").unwrap();
assert!(token_file_present(&token), "non-empty file");
assert!(!token_file_present(dir.path()), "directory");
}
/// The sweep runs as `hive-core`, which cannot read the agent-owned
/// 0600 token file. `chmod 000` does not stop root, so this test uses
/// content that `read_to_string` rejects instead: the predicate must
/// still report the file as present, which holds only if it never
/// reads the content.
#[test]
fn token_file_present_does_not_read_the_content() {
let dir = tempfile::tempdir().expect("tempdir");
let token = dir.path().join("matrix-token");
std::fs::write(&token, [0xff, 0xfe]).unwrap();
assert!(std::fs::read_to_string(&token).is_err());
assert!(token_file_present(&token));
}
}

View file

@ -539,36 +539,25 @@ async fn handle_matrix_create_user(
password: Option<&str>,
) -> Result<HostResponse> {
require_matrix_present()?;
if agent_exists(name)? {
// The swarm mints an agent's account and stores its token where the
// agent reads it; a second minter here would replace that token on the
// same device.
anyhow::bail!(
"matrix create-user: '{name}' is an agent, and an agent's matrix account comes from \
the swarm: swarm-controller creates it and re-checks it every five minutes"
);
}
let as_token =
crate::matrix::read_appservice_token().context("read matrix appservice token")?;
let client = matrix_http_client()?;
let mut out = Vec::new();
if agent_exists(name)? {
if password.is_some() {
// Agents auth by access_token, never by password — the
// boot-sweep provisioning path doesn't accept one. Refuse
// rather than silently dropping it.
anyhow::bail!(
"matrix create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token"
);
}
crate::matrix::ensure_user_for(&client, name.as_str(), &as_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
let path = Coordinator::agent_notes_dir(name).join("matrix-token");
out.push(format!("matrix: provisioned agent user '{name}'"));
out.push(format!("token persisted at: {}", path.display()));
} else {
let effective_password = match password {
Some(p) => p.to_owned(),
None => crate::matrix::random_password().context("generate random matrix password")?,
};
let token = crate::matrix::provision_user_token(
&client,
name.as_str(),
&as_token,
&effective_password,
)
let token =
crate::matrix::provision_user_token(&client, name.as_str(), &as_token, &effective_password)
.await
.with_context(|| format!("matrix create-user {name}"))?;
out.push(format!(
@ -576,15 +565,12 @@ async fn handle_matrix_create_user(
));
out.push(format!("token: {token}"));
if password.is_some() {
out.push(
"password: set as supplied — use it to log into a matrix web client".to_owned(),
);
out.push("password: set as supplied — use it to log into a matrix web client".to_owned());
} else {
out.push(
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(),
);
}
}
Ok(HostResponse::messages(out))
}

View file

@ -286,15 +286,15 @@ impl From<ReconcileFrom> for hive_host_sock::ReconcileDirection {
#[derive(Subcommand)]
pub enum MatrixCmd {
/// Create or refresh the matrix account + access token for `<name>`.
/// Create a matrix account for a person or other non-agent `<name>` and
/// print its access token to stdout.
///
/// For an existing agent, persists the token to its state dir; for a
/// human/other account, prints the access token to stdout. Set a
/// password to enable matrix web-client login (otherwise it uses a
/// random throwaway).
/// Refuses an agent's name: its account comes from the swarm
/// (`swarm-controller` creates it and stores its token where the agent
/// reads it). Set a password to enable matrix web-client login
/// (otherwise it uses a random throwaway).
CreateUser {
/// Matrix localpart. For agents: the container/agent name.
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
/// Matrix localpart of a non-agent account — `mara`, `damocles`, etc.
name: String,
/// Set the account password to this string instead of a random
/// throwaway. Use this for operator accounts that need to log