feat(gateway): hivectl gateway user management + fix htpasswdFile assertion

Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.

Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.

Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
This commit is contained in:
atlas 2026-06-01 23:00:38 +02:00
commit 4bff450343
61 changed files with 1084 additions and 547 deletions

View file

@ -139,21 +139,25 @@ impl MatrixBridge {
Returns the new event id."
)]
async fn send_message(&self, Parameters(args): Parameters<SendMessageArgs>) -> String {
render(round_trip(DaemonRequest::SendMessage {
room: args.room,
body: args.body,
}).await)
render(
round_trip(DaemonRequest::SendMessage {
room: args.room,
body: args.body,
})
.await,
)
}
#[tool(
description = "Open (or reuse) a direct message room with `user_id` \
(@user:server) and post `body` to it."
)]
#[tool(description = "Open (or reuse) a direct message room with `user_id` \
(@user:server) and post `body` to it.")]
async fn send_dm(&self, Parameters(args): Parameters<SendDmArgs>) -> String {
render(round_trip(DaemonRequest::SendDm {
user_id: args.user_id,
body: args.body,
}).await)
render(
round_trip(DaemonRequest::SendDm {
user_id: args.user_id,
body: args.body,
})
.await,
)
}
#[tool(
@ -162,35 +166,40 @@ impl MatrixBridge {
standard clients."
)]
async fn send_reaction(&self, Parameters(args): Parameters<SendReactionArgs>) -> String {
render(round_trip(DaemonRequest::SendReaction {
room: args.room,
event_id: args.event_id,
key: args.key,
}).await)
render(
round_trip(DaemonRequest::SendReaction {
room: args.room,
event_id: args.event_id,
key: args.key,
})
.await,
)
}
#[tool(
description = "Reply to a specific matrix event in a room, threaded \
via m.in_reply_to. Returns the reply's event id."
)]
#[tool(description = "Reply to a specific matrix event in a room, threaded \
via m.in_reply_to. Returns the reply's event id.")]
async fn send_reply(&self, Parameters(args): Parameters<SendReplyArgs>) -> String {
render(round_trip(DaemonRequest::SendReply {
room: args.room,
event_id: args.event_id,
body: args.body,
}).await)
render(
round_trip(DaemonRequest::SendReply {
room: args.room,
event_id: args.event_id,
body: args.body,
})
.await,
)
}
#[tool(
description = "Mark a specific event as read for this agent. Updates \
#[tool(description = "Mark a specific event as read for this agent. Updates \
the room's unread indicator + sends a read receipt other \
participants can see."
)]
participants can see.")]
async fn mark_read(&self, Parameters(args): Parameters<MarkReadArgs>) -> String {
render(round_trip(DaemonRequest::MarkRead {
room: args.room,
event_id: args.event_id,
}).await)
render(
round_trip(DaemonRequest::MarkRead {
room: args.room,
event_id: args.event_id,
})
.await,
)
}
#[tool(
@ -209,28 +218,27 @@ impl MatrixBridge {
render(round_trip(DaemonRequest::ListRoomMembers { room: args.room }).await)
}
#[tool(
description = "Read the most recent N events from a matrix room \
#[tool(description = "Read the most recent N events from a matrix room \
(default 50, max 200). Returns each event's id, sender, timestamp, \
type, and best-effort plain-text body."
)]
type, and best-effort plain-text body.")]
async fn read_room(&self, Parameters(args): Parameters<ReadRoomArgs>) -> String {
render(round_trip(DaemonRequest::ReadRoom {
room: args.room,
limit: args.limit,
}).await)
render(
round_trip(DaemonRequest::ReadRoom {
room: args.room,
limit: args.limit,
})
.await,
)
}
}
#[tool_handler(
instructions = "Matrix client for an agent on a hyperhive swarm. Use \
#[tool_handler(instructions = "Matrix client for an agent on a hyperhive swarm. Use \
`send_message` to post in a joined room, `send_dm` to message a \
specific user, `send_reaction` to react with an emoji, `send_reply` \
to thread a reply, `mark_read` to acknowledge an event. Discover \
rooms with `list_rooms`, members with `list_room_members`, recent \
timeline with `read_room`. Room references accept ids (!abc:server) \
or aliases (#name:server); user references use @user:server."
)]
or aliases (#name:server); user references use @user:server.")]
impl ServerHandler for MatrixBridge {}
#[tokio::main]
@ -258,7 +266,13 @@ async fn main() -> Result<()> {
}
let bridge = MatrixBridge::new();
let service = bridge.serve(stdio()).await.context("serve MCP over stdio")?;
service.waiting().await.context("MCP service exited unexpectedly")?;
let service = bridge
.serve(stdio())
.await
.context("serve MCP over stdio")?;
service
.waiting()
.await
.context("MCP service exited unexpectedly")?;
Ok(())
}