Skip to content

fix(controller): grant Matrix power levels to human members on room join - #1210

Open
LUOSENGWA wants to merge 3 commits into
agentscope-ai:mainfrom
LUOSENGWA:fix/room-power-levels
Open

fix(controller): grant Matrix power levels to human members on room join#1210
LUOSENGWA wants to merge 3 commits into
agentscope-ai:mainfrom
LUOSENGWA:fix/room-power-levels

Conversation

@LUOSENGWA

@LUOSENGWA LUOSENGWA commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Grant Matrix power levels to human members on room join

Summary

Humans are invited into worker and team rooms by the human reconciler, and into project rooms by the Manager — but nothing ever grants them a Matrix power level. They sit at the implicit level 0 in every room they belong to, and 403 on every room operation: renaming the room, inviting a colleague, basic housekeeping. Rooms created before power levels existed at all fall back to the homeserver's strict defaults; the room works for the bots that own it and is unusable for the person it is meant for.

This PR makes the grant declarative and self-healing:

  • The human room reconcile now ensures the human's power level in every desired room — new rooms (after invite + join) and already-observed rooms alike. The observed-room pass is the healing path: legacy rooms are fixed on the first reconcile after deployment, with no manual backfill.
  • Level mapping comes from the existing spec.permissionLevel: level 1 (admin equivalent) → 100, co-owning the rooms; levels 2/3 (team/worker scoped) → 50, Matrix's default member authority. That includes rename, invite, and kick/ban/redact of members strictly below 50 (the workers — the manager/leader at 100 can never be acted against); the design doc documents and accepts this authority explicitly rather than raising the thresholds.
  • Exact reconciliation, full-content preservation. Provisioner.EnsureRoomPowerLevel reads the current m.room.power_levels (new matrix.Client.GetRoomState, admin identity; the state endpoint returns the content object directly and is decoded as such; 404 = state never set → empty), reconciles only the target user's entry to exactly the mapped level (a demotion 100 → 50 actually lowers it), and writes back the complete existing content with only that entry mutated — events, invite, notifications, users_default, state_default, ban/kick/redact and any extension field survive untouched. No write when the user already has exactly that level. Steady state is one GET per room per cycle and zero writes.
  • Non-fatal. A failed grant is logged and retried next cycle; the room is still recorded.

Project rooms are created by the Manager, not the Controller. create-project.sh already writes a power_level_content_override (manager + admin at 100, workers at 0) but had no way to lift a human operator; it now accepts an optional --grant-admin <u1,u2,...> that adds the given users (local parts or full Matrix IDs) at level 100 to the creation-time override.

What's included

  • internal/matrix/client.go (+ test) — Client.GetRoomState: reads a state event's content with the admin identity, decoding the wire response directly (the state endpoint returns the content object, not an event envelope); missing state yields (nil, nil), not an error.
  • internal/service/provisioner.goEnsureRoomPowerLevel (exact-level reconciliation, full-content-preserving write, idempotent).
  • internal/service/interfaces.go — method on the human provisioner interface.
  • internal/controller/human_reconcile_rooms.go — the desired-room loop grants the level in every room, new and observed.
  • internal/controller/human_controller_test.go — level mapping (1→100, 2→50), observed-room healing, non-fatal failure.
  • internal/service/provisioner_power_test.go (new) — legacy room, merge preserving other users, extension fields (events/invite/notifications) surviving the write, exact-match no-write, 100 → 50 demotion actually lowering the user, read error, state without a users map, second grant preserving the first.
  • test/testutil/mocks/ — interface method + call recording on both provisioner mocks; the team-room test fake gains a stateful, JSON-round-tripped power-level store.
  • manager/agent/skills/project-management/scripts/create-project.sh — optional --grant-admin.
  • manager/agent/skills/project-management/references/create-project.md — the runtime-facing reference now instructs the Manager to pass --grant-admin for human operators who need room administration.
  • tests/check-create-project-grant-admin.sh (+ registration in .github/workflows/helm-lint.yml) — contract check that the flag stays implemented in the script and documented in the runtime reference.
  • docs/design/room-power-levels.md — design contract, including the explicit acceptance of the level-50 authority.

Data boundary

  • No CRD change, no new endpoints; the mapping derives from the existing spec.permissionLevel.
  • Worker / team / DM room creation keeps its existing power levels; worker service accounts stay at level 0 and still cannot manage rooms.
  • The grant never touches any other user's level and never rewrites the room's non-user power settings; only the target user's entry is mutated, so it is idempotent and converges (a demoted human is actually lowered on the next reconcile).
  • Legacy project rooms (created before --grant-admin existed) are healed by a one-time Manager-side operation, documented in the design doc — not automated here, since the Controller does not create or own project rooms.

Tests

  • New + extended unit tests as listed above (including the 100 → 50 demotion/revocation case and the extension-field preservation case); go test ./internal/matrix/ ./internal/service/ ./internal/controller/ green.
  • Full go test ./internal/... ./cmd/...: all packages ok except a pre-existing environment issue in internal/executor (unzip binary absent in the test container) untouched by this diff. gofmt/go vet clean on the diff; bash -n and the new contract check pass.

Related

  • Design: docs/design/room-power-levels.md.

人类成员房间权限授予(room join 时)

摘要

human 被 human reconciler 邀请进 worker/team 房间、被 Manager 拉进项目群——但没有任何步骤授予他们 Matrix 权限等级。他们在所有房间里恒为隐式 0 级,任何房间操作(重命名、邀请、基本维护)都 403。更早创建、从未设过 power levels 的房间则回退到 homeserver 严格默认值。房间对拥有它的 bot 好用,对真正要用的人不可用。

本 PR 把授权变成声明式且自愈:

  • human 房间 reconcile 现在对 desired 集合里的每个房间(新房间 invite+join 后、已 observed 的房间)确保 human 的权限等级。已观察房间的通道就是自愈路径:部署后第一轮 reconcile 即修复存量 legacy 房间,零手工 backfill。
  • 级别映射取自既有 spec.permissionLevel:level 1(admin 等价)→ 100(co-owner);level 2/3(团队/worker 范围)→ 50(Matrix 默认成员权限)。这包括 rename、invite,以及对严格低于 50 级成员(即 worker;100 级的 manager/leader 永远不可被其操作)的 kick/ban/redact——设计文档已明确记录并接受该权限,而非抬高阈值。
  • 精确对账、全量内容保留Provisioner.EnsureRoomPowerLevel 读当前 m.room.power_levels(新 matrix.Client.GetRoomState,admin 身份;state 端点直接返回 content 对象、按此解码;404 = 从未设置 → 空态),只把目标用户的条目对账到精确的映射级别(降级 100 → 50 就是真的降),写回时保留完整既有内容、仅改动该条目——eventsinvitenotificationsusers_defaultstate_defaultban/kick/redact 及任何扩展字段原样存活。用户已在该精确级别时不写。稳态 = 每房间每周期 1 次 GET、0 写。
  • 非致命:授权失败记日志、下轮重试,房间照常入 status。

项目群由 Manager 而非 Controller 创建。create-project.sh 本就写 power_level_content_override(manager + admin 100、worker 0),但没有给 human 操作员的通道;现新增可选参数 --grant-admin <u1,u2,...>,把指定用户(localpart 或完整 Matrix ID)以 100 级加入建群时的 override。

包含内容

  • internal/matrix/client.go(+测试)— Client.GetRoomState:admin 身份读 state event 的 content,直接解码 wire 响应(state 端点返回 content 对象、非 event 信封);state 缺失返回 (nil, nil) 而非错误。
  • internal/service/provisioner.goEnsureRoomPowerLevel(精确级别对账、全量内容保留写、幂等)。
  • internal/service/interfaces.go — human provisioner 接口方法。
  • internal/controller/human_reconcile_rooms.go — desired 房间循环对每个房间(新 + 已观察)授权。
  • internal/controller/human_controller_test.go — 级别映射(1→100、2→50)、存量房间自愈、失败非致命。
  • internal/service/provisioner_power_test.go(新)— legacy 房间、合并保留其他用户、扩展字段(events/invite/notifications)写后存活、精确匹配不写、100 → 50 降级真的把用户降下来、读错误、无 users 表的 state、第二次授权不覆盖第一次。
  • test/testutil/mocks/ — 两个 provisioner mock 的接口方法与调用记录;team-room 测试 fake 增加 stateful、JSON 往返仿真的 power-level 存储。
  • manager/agent/skills/project-management/scripts/create-project.sh — 可选 --grant-admin
  • manager/agent/skills/project-management/references/create-project.md — runtime 面向的参考文档现在指示 Manager 为需要房间管理的 human 操作员传 --grant-admin
  • tests/check-create-project-grant-admin.sh(+ .github/workflows/helm-lint.yml 注册)— 契约检查:该 flag 在脚本中保持实现、在 runtime 参考文档中保持有文档。
  • docs/design/room-power-levels.md — 设计契约,含对 50 级权限的明确接受说明。

数据边界

  • 无 CRD 变更、无新端点;映射取自既有 spec.permissionLevel
  • worker/team/DM 房间创建时的既有 power levels 不变;worker 服务账号保持 0 级、仍不能管理房间。
  • 授权从不触碰任何其他用户的级别、从不改写房间的非用户 power 设置;只改动目标用户自己的条目,故幂等且收敛(被降级 human 会在下轮 reconcile 被真的降下来)。
  • 存量项目群(--grant-admin 出现前创建)由 Manager 端一次性操作修复(设计文档有记录)——不在此 PR 自动化,因为 Controller 不创建也不拥有项目群。

测试

  • 上文所列新增/扩展单测(含 100 → 50 降级/撤销用例与扩展字段保留用例);go test ./internal/matrix/ ./internal/service/ ./internal/controller/ 全绿。
  • 全量 go test ./internal/... ./cmd/...:除 internal/executor 预存环境问题(测试容器缺 unzip,与本 diff 无关)外全部 ok。diff 内 gofmt/go vet 干净;bash -n 与新增契约检查通过。

相关

  • 设计文档:docs/design/room-power-levels.md

@LUOSENGWA
LUOSENGWA force-pushed the fix/room-power-levels branch 2 times, most recently from 128e0d8 to c4191f1 Compare September 2, 2026 06:08

@shiyiyue1102 shiyiyue1102 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. The goal is valid, but there are several blocking authorization/state issues:

  1. GET /rooms/{roomId}/state/{eventType} returns the state content object directly by default. GetRoomState instead unwraps a content field, and its test mocks a full event envelope. A compliant homeserver response is therefore read as nil. Please decode the actual wire response directly and update the test.

  2. EnsureRoomPowerLevel implements “at least” semantics. If a Human changes from permissionLevel 1 (100) to 2 (50), the existing level 100 is retained. Please reconcile the exact desired level and add a 100→50 revocation test.

  3. The write reconstructs the power-level content and drops fields including events, invite, notifications, and possible extension fields. Please preserve the complete existing content and mutate only the target users entry.

  4. Level 50 does not match the documented authority. Matrix defaults kick, ban, and redact to 50, so an L2/L3 Human can act against lower-level workers. Please either explicitly accept/document this authority or configure thresholds that enforce the intended permission model.

The project-room path should also be wired into the runtime-facing project-management reference; currently the Manager is never instructed to pass --grant-admin. Please add coverage and resolve or rerun the failing qwenpaw/qwenpaw integration shard before re-review.


感谢更新。目标合理,但当前有几个阻塞性的授权与状态问题:

  1. GET /rooms/{roomId}/state/{eventType} 默认直接返回 state content。当前 GetRoomState 又读取了一层 content,测试也错误模拟成完整 event,导致兼容规范的 homeserver 响应会被解析为 nil。请按真实响应格式解析并更新测试。

  2. EnsureRoomPowerLevel 使用“至少达到目标等级”的语义。Human 从 permissionLevel 1(100)降到 2(50)时仍会保留 100。请同步精确的目标等级,并增加 100→50 的权限撤销测试。

  3. 写回时重新构造了 power-level content,会丢失 eventsinvitenotifications 和扩展字段。请保留完整的原始 content,只修改目标 users 条目。

  4. Level 50 的实际权限与文档描述不符。Matrix 的 kickbanredact 默认门槛都是 50,因此 L2/L3 Human 可以操作低权限 Worker。请明确接受并记录这项权限,或者配置阈值以实现预期权限模型。

项目房间路径也需要接入 Agent 实际读取的 project-management reference;目前 Manager 不会被指示传递 --grant-admin。请补充覆盖,并在复审前处理或重跑失败的 qwenpaw/qwenpaw 集成测试。

@LUOSENGWA
LUOSENGWA force-pushed the fix/room-power-levels branch from c4191f1 to 146414d Compare September 2, 2026 15:59
@LUOSENGWA

Copy link
Copy Markdown
Contributor Author

@shiyiyue1102 All five points addressed — the force-push carries the fixes (new head 146414d1, same single commit, amended). Per item:

1. GetRoomState envelope mismatch. Correct. GET /rooms/{roomId}/state/{eventType}/{stateKey} returns the state content object directly; the implementation was decoding a content wrapper off the body (nil on any compliant homeserver), and the test mock mirrored the same wrong envelope shape — which is why it was green locally. The implementation now decodes the wire response directly as the content map, and the test mock now returns the bare content object, so the test pins the actual wire format.

2. "At least" semantics. Correct. The have >= level short-circuit meant a human demoted from permissionLevel 1 (100) to 2 (50) was never lowered. EnsureRoomPowerLevel now reconciles the target entry to exactly the mapped level — raising or lowering — with the no-write fast path only when the user already has exactly that level. Added TestEnsureRoomPowerLevel_DemotionRevokesLevel (100 → 50 writes and lands at 50; every other user untouched).

3. Write drops fields. Correct. The old write rebuilt the power-level content from a hardcoded whitelist, silently dropping events, invite, notifications, and any extension fields. It now preserves the complete decoded content and mutates only the target users entry. TestEnsureRoomPowerLevel_MergesExistingUsers now seeds events/invite/notifications and asserts they survive the write byte-for-byte.

4. Level-50 authority. Accepted and documented, as one of the two options you raised. Matrix defaults gate kick, ban, and redact at 50, so an L2/L3 human at 50 can kick/ban members strictly below 50 (the workers at 0 — never the manager/leader/admin at 100, which Matrix only allows against strictly lower levels) and redact in-room. I chose to accept and document rather than raise the thresholds: raising them would change the security posture of every room-creation path in the system (worker/team/DM/project), which is a system-wide policy change out of scope for this PR. docs/design/room-power-levels.md now carries an explicit "Level-50 authority — explicitly accepted" section with the rationale (the room's control plane sits at 100 above the human; workers are service accounts whose membership the reconciler re-invites, so kick/ban is reversible housekeeping; 50 is the Matrix ecosystem default). The reconciler code comment that made the wrong claim is corrected to match.

5. Project-room path not wired into the runtime reference. Correct — the flag was dead code for the Manager. The Manager reads manager/agent/skills/project-management/references/create-project.md at runtime; Step 1b there now documents --grant-admin with a concrete example and what it grants (level-100 co-ownership, and only for the listed humans). tests/check-create-project-grant-admin.sh (registered in helm-lint.yml, passing locally) pins both sides — the script implementation and the reference documentation — so this cannot silently drift again.

The force-push regenerates the full CI, including the qwenpaw/qwenpaw integration shard you asked to be resolved or re-run.


感谢反馈,五点全部修复(force-push 携带修复,新 head 146414d1,仍为单 commit,amend)。逐条:

1. GetRoomState 信封错位——属实。state 端点直接返回 state content 对象,实现却从响应体上解一层 content 包装(规范 homeserver 下恒 nil),而测试 mock 恰好也错配成同款信封——这就是本地全绿的原因。现改为直接解码 wire 响应为 content map;mock 同步改为返回裸 content 对象,测试从此钉住真实 wire 格式。

2. "至少"语义——属实。have >= level 短路导致 permissionLevel 1(100) 降到 2(50) 的 human 永远不会被降下。EnsureRoomPowerLevel 现把目标条目精确对账到映射级别(升或降),无写快速路径仅在用户已精确处于该级别时触发。新增 TestEnsureRoomPowerLevel_DemotionRevokesLevel(100 → 50 写入后落在 50;其他所有用户不动)。

3. 写回丢字段——属实。旧写回按硬编码白名单重建 power-level content,静默丢掉 eventsinvitenotifications 及一切扩展字段。现保留完整解码后的 content,只改动目标 users 条目;TestEnsureRoomPowerLevel_MergesExistingUsers 现种入三个扩展字段并断言写后原样存活。

4. Level-50 权限——按你给的两个选项之一处理:明确接受并文档化。Matrix 默认把 kick/ban/redact 门槛都设在 50,因此 50 级的 L2/L3 human 可对严格低于 50 的成员(0 级 worker;100 级 manager/leader/admin 因 Matrix 只允许操作严格更低级别而不可被触碰)执行 kick/ban,并可在房内 redact。选择接受而非抬阈值的原因:抬阈值会改变全系统每条房间创建路径(worker/team/DM/project)的安全态势,属系统级策略变更,超出本 PR 范围。docs/design/room-power-levels.md 现含显式 "Level-50 authority — explicitly accepted" 一节,写明权限边界与接受理由(房间控制面在 100 级、human 够不到;worker 是服务账号、成员资格由 reconciler 重新邀请,kick/ban 属可逆的运维手段;50 是 Matrix 生态默认门槛)。reconcile 代码里那句写错的注释已同步修正。

5. 项目房间路径未接 runtime 参考——属实,flag 对 Manager 是死代码。Manager 运行时读的是 manager/agent/skills/project-management/references/create-project.md,其 Step 1b 现已补上 --grant-admin 文档(含具体示例与授权说明:100 级 co-own,且仅授予所列 human)。新增 tests/check-create-project-grant-admin.sh(注册进 helm-lint.yml,本地 PASS)同时钉住脚本实现与参考文档两侧,防止再次静默漂移。

force-push 会重新生成全部 CI,包括你要求处理或重跑的 qwenpaw/qwenpaw 集成 shard。

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Grants Matrix power levels to human members on room join via a declarative, self-healing reconcile: EnsureRoomPowerLevel does a read-modify-write with full m.room.power_levels content preservation, mapping spec.permissionLevel 1 -> 100 and 2/3 -> 50, and the observed-room pass backfills legacy rooms without manual migration. The controller logic is correct and idempotent (exact-match short-circuit avoids writes in steady state; per-room errors follow the existing non-fatal reconcile policy), and test coverage is thorough. One actionable gap in create-project.sh (see inline): --grant-admin users get a power-level override but are never invited to the room, so they cannot join to exercise it.

Findings

  • [Warning] manager/agent/skills/project-management/scripts/create-project.sh:148 — --grant-admin users are added to power_level_content_override.users but never added to INVITE_LIST nor invited/auto-joined after creation, so the grant has no effect until they join via another path.
  • [Info] agentteams-controller/internal/matrix/client.go:760 — GetRoomState omits the trailing state key in the URL when empty, while SetRoomState always includes it; consider aligning for strict homeservers.
  • [Info] agentteams-controller/internal/service/provisioner.go:1074 — shallow copy shares nested maps between content and the fetched state; safe today, but a defensive copy would guard against future caching.
  • [Info] agentteams-controller/internal/controller/human_reconcile_rooms.go:44 — healing pass issues one GET per desired room every reconcile cycle; documented trade-off, could be optimized later with a status/generation check.

With the create-project.sh invite gap addressed, this PR looks ready to merge.


Automated review by github-manager-bot

@@ -129,7 +148,7 @@ ROOM_RESP=$(curl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRo
"power_level_content_override": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users passed via --grant-admin are added to power_level_content_override.users at level 100, but they are never appended to INVITE_LIST and no post-creation /invite or auto-join step is added for them (unlike the admin user which gets both an invite entry and an explicit /invite + auto-join block). The grant-admin users will therefore not be members of the project room after creation — the power level entry only takes effect when they eventually join through some other path. Fix: inside the --grant-admin loop, also append each ${grant_id} to INVITE_LIST (before the closing ] is added on the existing line 117 equivalent), and add a post-creation invite/auto-join step analogous to the existing admin auto-join block.

@@ -753,6 +760,38 @@ func (c *TuwunelClient) SetRoomState(ctx context.Context, roomID, eventType, sta
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRoomState builds the URL as state/{eventType} when stateKey is empty, while the existing SetRoomState always uses state/{eventType}/{stateKey} (producing a trailing slash for empty stateKey, e.g. state/m.room.power_levels/). Both forms are valid per the Matrix spec and most homeservers accept either, but the inconsistency could matter against strict implementations. Consider aligning GetRoomState to the same state/%s/%s pattern used by SetRoomState for symmetry, or document the deliberate difference.

@@ -1074,6 +1074,45 @@ func (p *Provisioner) EnsureRoomNonMember(ctx context.Context, roomID, userID, r
return p.matrix.KickFromRoom(ctx, roomID, userID, reason)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shallow copy content[k] = v means nested objects (e.g. events, notifications maps) are shared between content and the cur map returned by GetRoomState. This is safe today because cur is freshly unmarshalled per call and not cached, and only the top-level users key is mutated. However, if a future caller or middleware caches GetRoomState results, the shared nested maps could be silently corrupted. A defensive deep-copy of cur (or at minimum of the users sub-map before mutation) would be more robust against future refactoring.

@@ -44,29 +44,43 @@ func (r *HumanReconciler) reconcileHumanRooms(ctx context.Context, s *humanScope
next := make([]string, 0, len(h.Status.Rooms)+len(desired))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The healing pass calls EnsureRoomPowerLevel (one GET, conditional PUT) for every desired room on every reconcile cycle, even when nothing has changed. For a human in N rooms this adds N Matrix API GETs per 5-minute requeue. The PR documents this trade-off and the exact-match short-circuit avoids writes, but consider adding a status annotation (e.g. a lastPowerLevelGranted map in status) or a generation check to skip the GET when spec.permissionLevel hasn't changed since the last successful grant. This is a future optimization, not a blocker.

Humans are invited into worker/team rooms by the human reconciler and into
project rooms by the Manager, but nothing ever grants them a Matrix power
level: they sit at the implicit 0 and 403 on every room operation. Make
the grant declarative and self-healing.

- Human room reconcile ensures the mapped level in every desired room
  (new + already-observed; the observed pass heals legacy rooms on the
  first cycle after deployment)
- Level mapping from spec.permissionLevel: 1 -> 100 (co-owner),
  2/3 -> 50 (Matrix default member authority; the level-50 kick/ban/
  redact scope over sub-50 members is explicitly accepted and
  documented in docs/design/room-power-levels.md)
- EnsureRoomPowerLevel reconciles only the target users entry to exactly
  the mapped level (a demotion 100 -> 50 actually lowers it) and writes
  back the complete existing content with only that entry mutated --
  events/invite/notifications/extension fields survive; no write when
  already at exactly that level
- matrix.Client.GetRoomState reads a state event's content with the
  admin identity, decoding the wire response directly (the state
  endpoint returns the content object, not an event envelope); 404 ->
  (nil, nil)
- create-project.sh: optional --grant-admin <u1,u2,...> lifts the given
  humans to level 100 in the project room's creation-time override, and
  the runtime-facing project-management reference now instructs the
  Manager to pass it; tests/check-create-project-grant-admin.sh (wired
  into helm-lint) keeps the flag implemented and documented

Tests: wire-format GetRoomState mock, exact-match no-write, 100 -> 50
demotion/revocation, extension-field preservation, merge preserving
other users, legacy room, read error, state without a users map, second
grant preserving the first; controller-level mapping/healing/non-fatal
cases.
@LUOSENGWA
LUOSENGWA force-pushed the fix/room-power-levels branch from 146414d to 4b474a9 Compare September 4, 2026 06:33

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of the amended head (4b474a9): all four blocking items from the maintainer's CHANGES_REQUESTED review are addressed, and the previous review's --grant-admin invite gap is fixed. Verified against the code, and go test ./internal/matrix/ ./internal/service/ ./internal/controller/ is green locally; the previously failing integration-tests (controller-cr, SHARD_C_TESTS, qwenpaw, qwenpaw) CI shard now passes on this PR.

Verification of the maintainer's blocking items:

  1. Wire decodingGetRoomState (internal/matrix/client.go) decodes the state-endpoint response as-is as the content object (no envelope unwrapping), 404 → (nil, nil), and the state-key path segment is now always included (matching SetRoomState).
  2. Exact-level semanticsEnsureRoomPowerLevel writes exactly the mapped level and performs no write when the user is already at exactly that level; demotion is covered by TestEnsureRoomPowerLevel_DemotionRevokesLevel (100 → 50 actually lowers the user).
  3. Full-content preservation — the reconcile copies every existing content key and mutates only the target users entry, so events/invite/notifications/extension fields survive.
  4. Level-50 authority — explicitly documented and accepted with rationale in docs/design/room-power-levels.md.

Additional verification:

  • create-project.sh now adds --grant-admin users to the room invite list (previous review's Warning resolved) and gives them the level-100 override at creation.
  • The runtime-facing project-management reference now instructs --grant-admin, and a contract check is registered in the helm-lint workflow.
  • CI: all checks pass, including the previously failing qwenpaw/qwenpaw integration shard.

Automated review by github-manager-bot

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants