Skip to content

feat(matrix): add opt-in room administration - #126289

Draft
iainlane wants to merge 143 commits into
NousResearch:mainfrom
iainlane:feat/matrix-room-admin
Draft

iainlane wants to merge 143 commits into
NousResearch:mainfrom
iainlane:feat/matrix-room-admin

Conversation

@iainlane

@iainlane iainlane commented Sep 28, 2026 •

Copy link
Copy Markdown

Stack

All campaign PRs in this section target main. This list records logical merge order. Nesting means the parent PR must merge before its child. Every prerequisite for a dependent must merge first. Only adjacent layers are shown; each linked PR description lists its other dependencies.

Summary

This PR adds room administration to the opt-in matrix_admin toolset, which the pins layer introduced. The toolset is restricted to Matrix and disabled by default. Enable it for Matrix in hermes tools or in platform_toolsets.matrix. This replaces the source proposal's ambient credentials, raw HTTP client, MATRIX_TOOLS_ALLOW_ROOM_CREATE gate and core tool registration. The feature commit preserves Stefanos Anastasiou's authorship and Iain Lane's contributor credit.

matrix_room_admin has five actions:

  • create makes a private room with an optional name, topic and invite list. It can invite only the requester and users whom the gateway authorizes (MATRIX_ALLOWED_USERS or GATEWAY_ALLOW_ALL_USERS in the receiving bot's profile). encrypted: true adds Megolm encryption state and requires the owning client's active crypto store. is_direct: true requires exactly one invitee and records the room in the bot's m.direct account data.
  • invite invites a full Matrix user ID to the current room.
  • leave leaves the current room and removes it from the adapter's joined-room cache, so an authorized user can invite the bot back later.
  • forget forgets the current room after the bot has left. It affects only the bot's account and does not delete the room or other members' history.
  • redact redacts an event in the current room.

matrix_pin keeps its pin and unpin behavior and runs through the same checks.

Permissions

Each action uses the live adapter that received the event, with its SDK client, crypto store and event loop. Actions other than create apply only to the session's current room. The requester must be an authorized, joined room member, and must also have the Matrix power level for the change:

Action Requester needs
invite the room's invite level
leave, forget the room's kick level, unless the requester is the only other joined member (a private chat with the bot)
redact own event the send level for m.room.redaction (falling back to events_default)
redact another sender's event, including the bot's that level and the room's redact level
pin, unpin the send level for m.room.pinned_events
create no room power level

Power levels, encryption and the create event are read as raw JSON and evaluated with the spec defaults, through the same permission calculation that matrix_read kind=permissions uses. That result now also reports send_redaction. A room version 12 creator passes every power check. A requester whose level cannot be determined is refused. A refusal reports the required and actual levels. The homeserver then checks the bot's own permission, and its errors are returned with the Matrix error code and message. The requester's level is not compared with the bot's, because a bot that created a room usually has level 100 and even the room's moderators could not remove it.

The final power-level response is followed by synchronous ownership, requester, membership and room-identity checks before dispatch. No awaited access check can make that response stale before the write. If a room that was admitted as a direct chat becomes a group room during an action, the action is refused. For a NaN identity-cache setting, the room identity is refreshed before the final power read and checked again synchronously afterwards. This covers a room outside MATRIX_ALLOWED_ROOMS that is allowed only as a direct chat, and the private-chat exemption for leave and forget.

Ownership, profiles and interruption

When a shared bot routes a conversation to another profile, administration still uses the bot that received the event and runs in that bot's profile scope. Both the runtime profile and the receiving bot's profile must enable matrix_admin. The selection is captured in a worker thread before the final permission reads. Synchronous checks then compare the configuration files and resolver inputs with that snapshot before dispatch. An unstable snapshot permits at most one further attempt. The receiving profile's secrets are loaded in a worker thread before the action starts. Entering the profile scope still installs the profile's terminal policy on the gateway loop, which reads that profile's config.yaml, as it does for every gateway turn. Every other check compares in-memory state: the session, client, crypto object, registered owner, room policy and requester authorization. A change to any of them rejects the operation, including a client replacement between scheduling and the start of the operation. The tool runtime and the administration module share one owner snapshot.

If a check fails after the homeserver has accepted the write, the result reports the completed write with a warning. Interruption, timeout and cancellation wait until the owning task can no longer start a write. When the request may already have reached the homeserver, because the turn was interrupted, the deadline passed, or the client call failed without a Matrix error response, the result says that the outcome is unknown and gives an action-specific way to check before retrying. A leave that may have reached the homeserver removes the room from the joined-room cache; the next sync adds it back if the bot is still joined.

Administration relies on the routing provenance of resumed Matrix turns, which the parent stack keeps when build_session_context copies the source from a session entry. This layer no longer changes gateway/session.py.

Changing the toolset selection affects tool discovery in new sessions. Existing conversation history and system prompts are not rewritten.

Review scope

The PR targets main. Merge the prerequisites in the Stack section first. The signed linear history preserves the original author and committer identities, dates, timezones and complete messages, including contributor trailers.

Review the own layer with the scoped comparison. The comparison starts after the inherited pin contributions.

Current read-fixture lifetime

This revision incorporates the independently published #125732 read-fixture cleanup into the inherited inspection contribution. All seven worker dispatch calls use the same complete helper. The helper closes the worker event loop in its owning thread after dispatch returns or raises. The room branch retains its explicit routing-identity binding, every behavioural assertion and the cancellation sequence. Production source is unchanged.

The first 129 signed records are reused exactly. The correction is folded into the inspection contribution at record 130; only the necessary fourteen-record suffix receives new signatures. Every original author and committer field, date, timezone and complete message prefix remains intact. The suffix's empty credit contribution remains a separate record.

The complete current read-tool file reproduces two strict resource failures before this correction, with fifteen passing cases. The corrected complete file passes all seventeen cases, with actual SDK imports, one worker, zero retries and four strict warning categories. Full child output through interpreter shutdown has no resource warnings. Its 8,423 captured tracked Python and runner inputs match the actual source before and after execution.

Together with the previously accepted six complete files, the source-qualified scope is 688 passes across seven files and six existing tool-config skips. The earlier six-file executions remain valid because every production and selected test module is unchanged; only the previously unselected read-test module changes. This is a qualified combination, not a fresh simultaneous seven-file execution.

The changed complete module has twenty-one inherited type findings, compared with twenty-eight before the correction. The complete 103-path source-qualified comparison therefore contains 1,523 inherited findings. No finding is introduced. Ruff and changed-line whitespace pass. All 88 existing formatter paths remain inherited. The original six-file and static results below retain their recorded source qualifications.

Earlier source-qualified verification

The branch is rebased onto the captured upstream revision 3637c512fc3211bdccb61b7140d09ac1018508ba because the previous head had a concrete conflict in the inbound source-note helper. The helper retains the separate Matrix and Discord notes and uses upstream's removeprefix operation. All other production paths follow the automatic source composition.

The linear history preserves all 143 original records, literal author and committer fields, dates, timezones, complete original message prefixes and three meaningful empty credit records. The read-context EOF correction is folded into its original contribution. The room-administration tests close each executor thread's model-tools event loop after the operation finishes, including cancellation cleanup. That correction is folded into the contribution which first introduced the test callers. Test assertions are unchanged. The previously published Matrix SessionStore finalizers remain intact.

The complete room-administration file passes all 228 cases with the installed SDK, isolated home, four strict warning categories and no retries. Complete child output includes interpreter shutdown and reports no unclosed loop, socket, transport or database. The changed complete test module has zero type findings; Ruff passes and its existing formatter differences remain.

The pin fixture cleanup comes from the independently published #125882 source (7c31c52b) and is folded into its original pin contribution, at record 133. The complete worker dispatcher, all five worker call sites and main tool-loop cleanup match that public correction. Profile routing, requester permission fixtures, cancellation ordering and every assertion remain unchanged. The fresh complete pin file passes all 31 cases with zero resource warnings.

Across the affected six complete files, the source-qualified result is 671 passes and six existing tool-config skips. The room-admin and pin files were freshly executed after their corrections. The other four complete children, with 412 passes, are reused after verifying that all production and test source outside those two fixture modules is unchanged. Every child uses the canonical runner, actual SDK, zero retries and four strict warning categories. No database, loop, socket, transport, thread or unraisable-exception warning remains. This is a qualified combination of complete-file executions.

The initial full six-file execution, which reproduced the room and pin resource-lifetime failures, remains recorded as the failing control. The complete 103-path static comparison is reused for its unchanged source; both modified fixture modules were checked again and have zero type findings. The same 88 inherited formatter paths remain. The fixture cleanup adds no production behaviour.

The full 103-path Python comparison reports 1,530 findings on the composed source, compared with 1,518 on the preceding published branch and 1,436 on the captured upstream source. Complete literal diagnostic spans, columns, whole defining statement ASTs and occurrence counts show no new finding. Ruff passes. The same 88 paths retain inherited formatter differences. These are source comparisons, not a global type or formatting pass.

Hosted CI approval, maintainer acceptance, Windows execution and live homeserver checks remain separate.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets platform/matrix Matrix adapter (E2EE) labels Sep 28, 2026
Matrix permalinks change with each triggering event. Rendering one in
the pinned session prompt made the prompt stale or changed its bytes
every turn, which breaks conversation prompt caching.

Put the link in the current model-facing user message and remove it from
the durable transcript, including queued turns. Respect effective PII
redaction when adding the note. Percent-encode room IDs, event IDs and
the via server so delimiters cannot change the matrix.to URL structure.

Preserve the Matrix and Discord source-note handling when integrating
the upstream prefix removal change. Use removeprefix after selecting
the platform-specific note, so each transport removes only its own
generated prefix.
The live Matrix exchange now checks the model request for a permalink to
the event sent by the independent client. It also verifies that the link
is absent from the system prompt, which must remain stable across turns.
Record the exchange duration separately from container setup time.
Room version 12 IDs have no server component. A permalink built only
from the room ID therefore omitted the server hint, which can prevent a
client outside the room from locating it.

Use the joined bot account's server when the room ID has no domain. The
builder still leaves via absent when neither server is known.
A room ID can identify a server that no longer participates in the
room. Its server may also include a port or IPv6 address, which the
last-colon parser truncated to an invalid via hint.

Use the joined bot account's server for inbound source links. When only
a room ID is available, parse its full server after the first colon.
The same parse also corrects the room's session scope.
In a thread, every turn's source link pointed at the thread root. The
root was chosen as a stable anchor while the link lived in the pinned
session prompt, which is per thread. The link now goes on each user
turn, so the reason no longer applies, and the model never received the
ID of the message that triggered the turn. A request to cite "this
message" produced a link to the root, which may be another user's
message.

Build the per-turn link from the triggering event in all cases. A
matrix.to link to an event inside a thread opens that thread in current
clients, and the session prompt already gives the thread root.
A room-ID permalink needs via servers so that a reader outside the room
can join through one of them. The link carried only the bot's server.
When that server was unreachable from the reader's server, the link
failed.

Choose up to three servers as the Matrix specification recommends for
room-ID permalinks. The first is the server of the highest-power joined
member at power level 50 or above; room version 12 creators count as
the highest power. The rest are the servers with the most joined
members, and IP literals are never chosen. The adapter reads members,
power levels and the create event from the mautrix state store, which
sync keeps current, so building a link makes no request. When the
store has no joined members for the room, use the bot's server and then
the room ID's server.

Server ACLs are not applied, because the state store does not keep
m.room.server_acl. Move the builder and the selection into a sibling
module, and emit one via parameter per server.
The session prompt stopped rendering the Matrix source link when the
link moved to the current user turn. Two prompt tests still asserted
that the prompt omitted the link when it was unset or redacted. No code
path produces that output any more, so neither test could fail because
of this feature. The prompt-stability test covers the remaining
contract, and the per-turn tests cover the note and its PII
suppression.

Restore the comment on the per-turn notes. strip_inbound_source_note
removes a note only when it is the first thing in the message, so a
later prefix added outside the notes would leave the note in the
persisted transcript.
Choosing source-link via servers made every inbound message await
get_power_levels and get_create on the client's state store. The
project-context isolation and voice tests gave the adapter a MagicMock
store with only get_members awaitable, so all six inbound tests in
those files failed with "'MagicMock' object can't be awaited".

Production always uses mautrix's MemoryStateStore. Give both fakes a
real MemoryStateStore in which the bot and the sender have joined the
test rooms. The real store implements every state query, so these tests
keep working when the adapter starts reading other room state. The
stores are filled with MemoryStateStore.deserialize because the helpers
are synchronous and some are called inside a running event loop.
The effective-state parent added a description to the matrix_read limit
argument: "Maximum number of events to read". In this layer the same
argument also bounds the members kind, which returns members rather
than events, and the pins kind. Say that the limit applies to events or
members so the model can tell that it bounds member lists too.
The merged matrix_read description said that events are listed oldest
first and that `errors` lists events that could not be decrypted. Both
sentences came from the history reads. A pins read returns events in
the order of the room's m.room.pinned_events list, and its errors also
cover pins that could not be fetched, are not visible messages, came
back as another event, or whose edit was redacted. A model that read
the description could tell a user that a deleted pin was an encrypted
message.

Limit the ordering sentence to room and thread reads, say that pins
follow the room's pinned order, and say that `errors` lists events that
could not be read or decrypted, with the reason. History reads already
returned non-decryption errors such as "event content changed", so the
wider wording is also accurate for them.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 6679ac8 to bcaf78e Compare October 1, 2026 09:36
Room classification controls the Matrix room allowlist and mention
gates.
The previous member-count shortcut classified a bot-only room as a DM,
and an m.direct entry classified larger or unreadable rooms as DMs.
Those rooms could bypass both gates.

Classify a room as a private bot chat only when the bot and exactly one
other user are joined. Request JOIN membership explicitly: mautrix's
state store includes invited users by default. The state store may also
contain only a subset of a room's members, so use its joined IDs only
when has_full_member_list confirms completeness. Otherwise query the
homeserver with get_joined_members and apply room rules if that fails.

Keep m.direct as room identity metadata, without using it for
authorisation. Document the resulting allowlist and mention policy.

Tests reproduced the bot-only, bot-absent, multi-user, invited-user,
partial-cache and unknown-membership cases before their fixes.
The DM tests marked m.direct account data but did not supply joined
members. The two-person classifier therefore treated those fixtures
as rooms, so their threading assertions tested the wrong policy.

Give the fixture a full joined member list with the bot and one other
user. Keep the three-member variant for room behaviour. The mention
and threading tests now exercise the intended DM policy.
A room is a private bot chat only when the bot's user ID is one of its
two joined members. With an access token, the adapter replaces the
configured MATRIX_USER_ID with the ID that whoami returns. After a
password login it kept the configured string. Synapse accepts a login
identifier whose localpart differs in case from the registered user, so
a configured @Hermes:example.org logs in as @Hermes:example.org. The
membership check then never matched, and every one-to-one chat followed
room rules: with MATRIX_ALLOWED_ROOMS set its messages were dropped,
and otherwise they needed a mention.

mautrix's login stores the homeserver's user ID in client.mxid. Copy it
into the adapter after a successful login, as the whoami path does, so
the membership check uses the ID that appears in the room's member
list.
The Matrix handoff session-key test marked its DM case only through the
m.direct cache. Room classification no longer reads m.direct, so the
DM case was keyed as a group and the test failed. Give the fake client
a joined member list: the bot and one other user for the DM case, and a
third user for the room case.
The environment-variable reference still said that any room with two or
fewer joined members is a DM, and told operators to add a third member
to get room behaviour. A bot-only room, a room without the bot and a
room whose membership cannot be read now follow room rules. Describe
the current rule in the MATRIX_ALLOWED_ROOMS, MATRIX_REQUIRE_MENTION,
MATRIX_FREE_RESPONSE_ROOMS and MATRIX_AUTO_THREAD rows.

The comment in _schedule_pending_invite_joins said that a direct invite
missing from m.direct is misclassified as a group. m.direct no longer
affects classification, so drop that clause.
The E2EE crypto store keys every row (the Olm account, Olm sessions
and inbound Megolm sessions) by an account ID taken from the adapter's
user ID. After a password login the adapter now replaces the configured
MATRIX_USER_ID with the homeserver's spelling of it, so the DM check
matches the room's member list. That replacement also changed the store
key. A deployment with a password login, MATRIX_USER_ID differing in
case from the registered user, and a stable MATRIX_DEVICE_ID would open
an empty store after upgrading. It would create a new Olm account for
its existing device and could no longer decrypt history. The same
happened on an in-process reconnect, because the second login read the
replaced ID.

Keep the configured ID in its own attribute and use it for the login
identifier and, after a password login, for the store key, as before.
The access-token path keys the store by the whoami answer, as it always
has. The adapter's user ID still becomes the homeserver's spelling on
both paths, and the DM, mention, invite and self-sender checks use it.

Preserve the HTTP API supplied to the password-login fixture client and
disconnect its first real connection before reconnecting. The fixture
keeps every configured-account and canonical-identity assertion. The
shared Client constructor preserves the supplied API for this fixture.
Matrix rooms often have no m.room.name state event. Identity
resolution used the opaque room ID as the name shown to the agent
even when other occupants had readable display names.

Read member profiles from the state store, with the joined members
API as a fallback when the store is empty. Use the other occupants
to title an unnamed room. Explicit-name detection and DM
classification remain unchanged.
Matrix room identity was cached without reacting to live state
changes. Topic edits, renames and membership changes could leave
stale room context. The agent also missed important privacy or
replacement changes.

Register sync-awaited state handlers that invalidate the identity
cache. Queue one note per change kind for the next ordinary message.
Keep notes through mention drops, commands and text batching. Quote
and bound room-state values before they enter channel context.
Room notes were consumed while the adapter built an event. The gateway
could reject that event for sender policy or intercept it for a paused
session, leaving no note for the next model turn. A queued follow-up
also bypasses normal admission and missed the note entirely.

Consume notes in inbound model-message preparation, which both cold
and queued turns use after their dispatch gates. Rejected and
intercepted messages therefore leave pending notes for the next turn.

Busy-session debounce and pending-message merges also discarded
channel context from later events. Append it during both merges so
accepted context reaches the model. Request joined member profiles
explicitly so invited users do not appear in unnamed room titles.

Runner admission, paused and queued turns, busy merges, room bounds,
coalescing and invited membership have focused regressions.
A Matrix state update can change a member-derived room title, an
explicit room name or the room topic between turns. Those changes
altered the session-context prompt and agent cache signature, which
rebuilt the agent and invalidated the provider's cached prefix.

Render the Matrix room name, topic and same-sender display name from
the session's persisted origin. Keep live state for routing and room
identity. Member changes join the existing per-turn room notes, which
also report explicit name and topic edits.

A regression reproduced prompt and signature drift on the previous
head. It now verifies stable bytes and signatures after a member
rename, a join, an explicit room rename and a topic edit.
Matrix conversations pin room metadata in their initial prompt. A
reset copied the previous origin, so a new conversation still used an
old room name or topic after a state change. Pass the live Matrix
source through explicit and automatic resets, validate its route, and
persist it as the fresh session origin.

Room state notes were consumed once per room. In rooms with several
threads, the first model turn received a change and the others kept
old prompt metadata without an update. Track delivery per session and
filter notes recorded before a new session began. Keep the room and
session cursor stores bounded.

Regressions cover stable old prompts, fresh reset prompts after a
restart, two existing threads, and a thread created after the update.
Matrix prompts keep their initial room metadata for the life of a
conversation. Room state notes, however, lived only in adapter memory.
A rename, topic edit or member change while the gateway was offline
left a restored conversation with old metadata and no correction.

Persist the room state last delivered to each Matrix session. On the
next prepared model turn, compare that state with the current room
identity and add bounded correction notes to the user message. Merge
live state-event notes by kind so the same change appears once. Hash
joined members and display names to detect membership changes without
storing their list in session metadata.

A real SessionStore restart regression covers two conversations in
one room, stable prompt bytes and one correction per conversation.
A second restart confirms that delivered corrections stay consumed.
A queued Matrix message can contain room metadata from arrival time even
when the room has changed again before the model turn. Resolving the
current identity from the owning adapter makes the correction and the
recorded state describe the same room snapshot. Current state replaces
stale queued notes for name, topic and membership changes.

Room-state acknowledgement was written during message preparation. If
the gateway stopped before the user transcript row was saved, a
restored session suppressed a correction that the model never saw.
Record the state on the user row instead. A failed preparation leaves
no acknowledgement; successful and failed agent turns use their
existing transcript persistence paths.

Regressions exercise a stale queued message and a crash before the
transcript write with a real SessionStore restart.
An earlier commit in this series appended every incoming event's
channel_context when the base adapter merged a text batch, a pending
message or a busy-session debounce. Matrix room notes were the reason,
but Matrix now attaches its notes when the turn is prepared, so the
change only affected Discord, Slack and relay.

Discord backfills a thread message by scanning back to the bot's last
reply. For two batched messages, the second backfill already contains
the first backfill and the first message, so the merged turn showed
the first backfill twice and the first message three times.

Restore the previous merge, which keeps the first event's context, and
add a Discord test for two batched thread messages.
Room context reached the model through Matrix-specific code in the
gateway core. run_inbound imported the Matrix plugin behind a
Platform.MATRIX check, run_turn read an ad hoc _matrix_room_state
attribute, and session.py pinned prompt metadata, validated resets and
kept a member digest on SessionSource for Matrix only. Plugins must use
a generic surface instead.

The Matrix adapter also kept a second mechanism: notes stashed from live
state events with per-session cursors. On the production path the
runner discarded those notes in favour of the snapshot comparison, so
only tests reached them.

Add BasePlatformAdapter.prepare_turn_context. The gateway calls it
while it prepares a turn, passing the session origin and the
channel_state saved with the latest user row. The adapter returns a
TurnContextUpdate with a note for the user message and a new snapshot.
The snapshot travels on MessageEvent.channel_state and is saved in the
user row's display_metadata, so a change is acknowledged only when the
turn that reported it is saved. An adapter that sets
reports_chat_changes_in_turn keeps the session origin's chat name,
topic and user name in the session-context prompt, which stays
byte-stable while the notes report later changes.

Matrix implements the hook with its existing snapshot comparison. The
live-note stash, its take methods and their tests are gone; the
remaining tests run through a real SessionStore. The state handlers
now only invalidate the cached room identity.

build_session_context no longer pins anything, so tools read the
current chat name through HERMES_SESSION_CHAT_NAME, and the prompt copy
uses session_identity.replace_source. reset_session refreshes the
origin's name, topic and user name from the /reset source for every
platform and keeps the origin's routing fields, which removes the
Matrix guard that could raise after /reset had already evicted the
agent. format_untrusted_prompt_value becomes public because the plugin
uses it.

The room-session test helpers retain every SessionStore they create or receive until pytest teardown, then release their shared database handles. This includes restarted room sessions. The finalizer applies only to these fixture helpers; every existing test body and assertion is unchanged.
Matrix turns could read message history, but the agent could not
inspect the current room state, joined members, power levels or pinned
messages. Those details are available from the receiving bot and can
affect what the agent should say or attempt.

Extend the Matrix-only read tool with bounded inspection kinds. Fetch
state and members through the owning adapter, apply its existing room
and requester policy, and resolve pinned messages with the same
decryption path as event reads. Report missing pin content per event
so the other pins remain visible. Include the event type and creator
override when presenting permission thresholds.

A separate Matrix client verifies that the model receives the room
details and pinned message from a disposable Synapse instance.

Use the effective-state parent's client-events decoder and gateway
scheduling path throughout inspection and its tests. Recheck admission
after identity resolution, derive permission defaults from raw state,
and preserve cancellation while refreshing pinned content. The feature
and these interface migrations form one change so the initial commit
uses the current prerequisite APIs.


Published read fixture worker-loop lifetime correction:

The read-tool tests dispatch through executor threads. Each worker can
create a thread-local event loop, but the fixture leaves that loop open
when the worker exits. Its selector sockets then produce strict resource
failures. The complete standalone file reproduces two such failures.

Use the published read fixture dispatcher for all seven worker calls.
It closes the worker loop in the same thread after dispatch returns or
raises. Keep every behavioural assertion and the existing cancellation
sequence unchanged. Production code and the prerequisite history do not
change.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 7, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from c4aa42d to 34b1801 Compare October 7, 2026 14:52
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 7, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 34b1801 to 95638fb Compare October 7, 2026 15:58
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 95638fb to 052f3d7 Compare October 8, 2026 08:20
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 8, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 052f3d7 to fe59078 Compare October 9, 2026 05:44
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
The fake HTTP API discarded the client_session supplied by the Matrix
adapter, so disconnect closed a mock while the real aiohttp session was
left open. A reconnect test also replaced disconnect with a stub and
left its new connection open after checking the guard. Strict resource
warnings expose both leaks.

Preserve the supplied session in the fake API. Use a wrapping disconnect
spy in the reconnect test and disconnect the replacement client after
the existing call assertion. The fake API and reconnect fixture predate
this branch, so this focused fixture maintenance is separate from the
corrections folded into their campaign contributors. Production code
and all original assertions are unchanged.

On the earlier Matrix prerequisite, prebuilt Client stubs also discarded
the supplied API. Use the same scoped constructor for the inherited
fixtures so disconnect closes the adapter-created session. This is the
earlier fixture layout; all original assertions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from fe59078 to a47e944 Compare October 9, 2026 06:40
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from a47e944 to 14c37b9 Compare October 9, 2026 09:21
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 14c37b9 to 8efcbf7 Compare October 9, 2026 22:10
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.
@iainlane
iainlane force-pushed the feat/matrix-room-admin branch from 8efcbf7 to cbc39df Compare October 10, 2026 00:07
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
The administration fixture replaces the room classifier with a function
that accepts only a room ID. Discovery now passes the fixed client owner
and classification requirement to that function. The allowlist helper
catches the resulting TypeError and refuses the room before the test can
change the membership during its power-level read.

Accept the production keyword arguments in the fixture. Keep the exact
original refusal and empty-write assertions for invite, redact, pin and
leave. All four cases pass on both the original prerequisite and the
composed control feature when the real late-reclassification path runs.

Historical source attribution for audit record 435.

The fixture correction from original source
a8ac3c29caef8bc2fd01e05912186da3828deb0f is already folded into
public owner 44e8969 on PR NousResearch#126289.
This empty child records the original contribution's attribution.
The source tree and all existing contributions remain unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/matrix Matrix adapter (E2EE) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants