Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: corteza
Title: AI Agent Runtime
Version: 0.6.9.3
Version: 0.6.9.4
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
email = "troy@cornball.ai",
Expand Down
43 changes: 41 additions & 2 deletions R/matrix.R
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ matrix_mx_session <- function(cfg) {
mx.client::mx_client_session(cfg)
}

# Re-login with the stored password and persist the refreshed token to
# corteza's config path. Reuses the device_id so the device (and any
# E2EE identity bound to it) survives the rotation.
matrix_relogin <- function(cfg) {
if (is.null(cfg$password) || !nzchar(cfg$password)) {
stop("matrix config has no stored password to re-login with",
call. = FALSE)
}
s <- mx.api::mx_login(cfg$server, cfg$user, cfg$password,
device_id = cfg$device_id)
cfg$token <- s$token
cfg$user_id <- s$user_id
cfg$device_id <- s$device_id
matrix_save_config(cfg)
}

#' Configure the Matrix channel for this host
#'
#' Logs in to a Matrix homeserver as the bot account, joins (or records)
Expand Down Expand Up @@ -746,8 +762,21 @@ matrix_poll <- function(system = NULL, model = NULL, provider = NULL,
cfg <- matrix_load_config()
mx_sess <- matrix_mx_session(cfg)

sync <- mx.api::mx_sync(mx_sess, since = cfg$sync_token,
timeout = as.integer(timeout))
# Self-heal an invalidated access token: re-login with the stored
# password (same device_id, so an E2EE identity survives), persist
# the refreshed config, and retry the sync once. Other errors
# propagate as before.
sync <- tryCatch(
mx.api::mx_sync(mx_sess, since = cfg$sync_token,
timeout = as.integer(timeout)),
mx_error_M_UNKNOWN_TOKEN = function(e) {
message("matrix_poll: token rejected; re-logging in")
cfg <<- matrix_relogin(cfg)
mx_sess <<- matrix_mx_session(cfg)
mx.api::mx_sync(mx_sess, since = cfg$sync_token,
timeout = as.integer(timeout))
}
)

first_run <- is.null(cfg$sync_token)
cfg$sync_token <- sync$next_batch
Expand Down Expand Up @@ -892,7 +921,17 @@ matrix_poll <- function(system = NULL, model = NULL, provider = NULL,
next
}

# Show a typing indicator while the model works -- turns run
# seconds to minutes, and the indicator is the only sign of
# life the other side gets. Best-effort: a failed typing call
# must never block the reply. 120s cap; Matrix clears it when
# the reply event arrives.
tryCatch(mx.api::mx_typing(mx_sess, m$room_id, TRUE,
timeout = 120000L),
error = function(e) NULL)
reply <- matrix_run_turn_in_cwd(m$body, session)
tryCatch(mx.api::mx_typing(mx_sess, m$room_id, FALSE),
error = function(e) NULL)
if (is.null(reply) || !nzchar(reply)) {
reply <- "(no reply)"
}
Expand Down
31 changes: 31 additions & 0 deletions R/matrix_crypto.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,42 @@ matrix_crypto_init <- function(cfg) {
crypto$encrypted <- matrix_crypto_load_encrypted(store)
crypto$self_curve <-
mx.crypto::mxc_account_identity_keys(acct)$curve25519
# Ask each joined room for its m.room.encryption state up front,
# instead of waiting for a sync to happen to mention it. Best-effort
# per room; a transient failure just defers that room to sync-time
# detection.
found <- matrix_crypto_scan_rooms(cfg)
new_rooms <- setdiff(found, crypto$encrypted)
if (length(new_rooms)) {
crypto$encrypted <- c(crypto$encrypted, new_rooms)
matrix_crypto_save_encrypted(crypto)
}
message("matrix_run: E2EE enabled (", length(crypto$encrypted),
" known encrypted room(s))")
crypto
}

# All joined rooms that advertise m.room.encryption, by direct state
# query (startup path; sync-time detection still runs in the poll loop).
matrix_crypto_scan_rooms <- function(cfg) {
s <- tryCatch(matrix_mx_session(cfg), error = function(e) NULL)
if (is.null(s)) {
return(character())
}
rooms <- tryCatch(mx.api::mx_rooms(s), error = function(e) character())
out <- character()
for (rid in rooms) {
enc <- tryCatch(
mx.api::mx_get_state(s, rid, "m.room.encryption"),
error = function(e) NULL
)
if (!is.null(enc$algorithm)) {
out <- c(out, rid)
}
}
out
}

matrix_crypto_load_encrypted <- function(store) {
f <- file.path(store, "encrypted_rooms.json")
if (!file.exists(f)) {
Expand Down
6 changes: 6 additions & 0 deletions inst/tinytest/test_matrix.R
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,9 @@ expect_false(corteza:::matrix_is_clear_command("don't /clear yet"))
expect_false(corteza:::matrix_is_clear_command("hello"))
expect_false(corteza:::matrix_is_clear_command(""))
expect_false(corteza:::matrix_is_clear_command(NULL))

# 0.3.0 adoption helpers exist with the expected shapes.
expect_true(is.function(corteza:::matrix_relogin))
expect_true(is.function(corteza:::matrix_crypto_scan_rooms))
expect_error(corteza:::matrix_relogin(list(server = "https://x")),
"no stored password")