Repository navigation
Conversation
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 1, 2026 09:36
6679ac8 to
bcaf78e
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 7, 2026 14:52
c4aa42d to
34b1801
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 7, 2026 15:58
34b1801 to
95638fb
Compare
This was referenced Oct 7, 2026
iainlane
force-pushed
the
feat/matrix-room-admin
branch
from
October 8, 2026 08:20
95638fb to
052f3d7
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 9, 2026 05:44
052f3d7 to
fe59078
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 9, 2026 06:40
fe59078 to
a47e944
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 9, 2026 09:21
a47e944 to
14c37b9
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 9, 2026 22:10
14c37b9 to
8efcbf7
Compare
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
force-pushed
the
feat/matrix-room-admin
branch
from
October 10, 2026 00:07
8efcbf7 to
cbc39df
Compare
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_admintoolset, which the pins layer introduced. The toolset is restricted to Matrix and disabled by default. Enable it for Matrix inhermes toolsor inplatform_toolsets.matrix. This replaces the source proposal's ambient credentials, raw HTTP client,MATRIX_TOOLS_ALLOW_ROOM_CREATEgate and core tool registration. The feature commit preserves Stefanos Anastasiou's authorship and Iain Lane's contributor credit.matrix_room_adminhas five actions:createmakes 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_USERSorGATEWAY_ALLOW_ALL_USERSin the receiving bot's profile).encrypted: trueadds Megolm encryption state and requires the owning client's active crypto store.is_direct: truerequires exactly one invitee and records the room in the bot'sm.directaccount data.inviteinvites a full Matrix user ID to the current room.leaveleaves the current room and removes it from the adapter's joined-room cache, so an authorized user can invite the bot back later.forgetforgets 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.redactredacts an event in the current room.matrix_pinkeeps 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
createapply 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:invitelevelkicklevel, unless the requester is the only other joined member (a private chat with the bot)m.room.redaction(falling back toevents_default)redactlevelm.room.pinned_eventsPower 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=permissionsuses. That result now also reportssend_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_ROOMSthat 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'sconfig.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_contextcopies the source from a session entry. This layer no longer changesgateway/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
3637c512fc3211bdccb61b7140d09ac1018508babecause 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'sremoveprefixoperation. 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.