feat(gateway): let write-scope operators manage chat session organization#100964
Conversation
…tion sessions.patch becomes a params-aware dynamic-scope method: operator.write now authorizes patches that touch only user-level chat-organization fields (label, category, pinned, archived, unread); every other field, mixed patches, and unknown keys keep requiring operator.admin (fail closed). This unblocks the session controls shipped in #100814 for the Android app, whose bounded operator session intentionally never requests operator.admin. Also: session list ordering gains a deterministic key tiebreaker for equal pinnedAt/updatedAt (stable offset paging and prompt-cache friendliness), a user-assigned label now beats stored channel-derived display names in the row projection so renames survive refreshes, and subagent spawn pins its gateway calls to admin via the params-aware least-privilege resolver instead of the static admin-only method check (#59428 contract preserved). Refs #100712
|
Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 11:34 AM ET / 15:34 UTC. Summary PR surface: Source +63, Tests +182. Total +245 across 8 files. Reproducibility: yes. Source inspection shows current main keeps Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land the field-allowlisted dynamic Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main keeps Is this the best way to solve the issue? Yes, pending maintainer approval. Reusing the existing patch schema with a fail-closed field allowlist is narrower than adding duplicate protocol surface, but the authorization boundary is a real product/security choice. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9606fe43231b. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +63, Tests +182. Total +245 across 8 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Merged via squash.
|
…tion (openclaw#100964) sessions.patch becomes a params-aware dynamic-scope method: operator.write now authorizes patches that touch only user-level chat-organization fields (label, category, pinned, archived, unread); every other field, mixed patches, and unknown keys keep requiring operator.admin (fail closed). This unblocks the session controls shipped in openclaw#100814 for the Android app, whose bounded operator session intentionally never requests operator.admin. Also: session list ordering gains a deterministic key tiebreaker for equal pinnedAt/updatedAt (stable offset paging and prompt-cache friendliness), a user-assigned label now beats stored channel-derived display names in the row projection so renames survive refreshes, and subagent spawn pins its gateway calls to admin via the params-aware least-privilege resolver instead of the static admin-only method check (openclaw#59428 contract preserved). Refs openclaw#100712
…tion (openclaw#100964) sessions.patch becomes a params-aware dynamic-scope method: operator.write now authorizes patches that touch only user-level chat-organization fields (label, category, pinned, archived, unread); every other field, mixed patches, and unknown keys keep requiring operator.admin (fail closed). This unblocks the session controls shipped in openclaw#100814 for the Android app, whose bounded operator session intentionally never requests operator.admin. Also: session list ordering gains a deterministic key tiebreaker for equal pinnedAt/updatedAt (stable offset paging and prompt-cache friendliness), a user-assigned label now beats stored channel-derived display names in the row projection so renames survive refreshes, and subagent spawn pins its gateway calls to admin via the params-aware least-privilege resolver instead of the static admin-only method check (openclaw#59428 contract preserved). Refs openclaw#100712
What Problem This Solves
The session controls shipped in #100814 (rename, pin, archive, mark unread, move to group) mutate sessions through
sessions.patch, which requires theoperator.adminscope. The Android app deliberately connects as a bounded operator without admin (nativeClientOperatorScopesinapps/android/.../node/ConnectionManager.kt), so every one of those controls fails authorization withmissing scope: operator.adminon Android. More broadly, user-level chat organization should not require admin rights on any client.Two smaller gaps in the same surface: session list ordering had no tiebreaker for equal timestamps (unstable offset paging), and a stored channel-derived
displayNamebeat a user-assignedlabelin the row projection, so renames of channel-named sessions vanished on the next refresh.Tracking issue: #100712
Why This Change Was Made
sessions.patchbecomes a params-aware dynamic-scope method (same mechanism asplugins.sessionAction):operator.writeauthorizes patches touching only user-level chat-organization fields —label,category,pinned,archived,unread(pluskey/agentIdtargeting). Any other field, mixed patches, or unknown keys keep requiringoperator.admin, fail closed. Write scope can already create sessions, send messages, and abort runs, so chat organization is not an escalation; the admin-only fields (sendPolicy, tool inheritance, exec routing, model, spawn metadata, …) stay admin-only. Existing handler guards are unchanged (no archiving active runs or an agent's main session; webchat mutations stay rejected).compareSessionEntryPairsgains a locale-independent key tiebreaker afterpinnedAt/updatedAt, so list order and offset paging are deterministic across calls.labelover storeddisplayName: an explicit user rename must win over channel-derived names.isAdminOnlyMethodcheck; withsessions.patchnow dynamic it uses the params-aware least-privilege resolver, so spawn-metadata patches still pin to admin (preserves the [Bug]: sessions_spawn sub-agent fails with "pairing required" (1008) on v2026.4.1 #59428 no-scope-upgrade contract).isAdminOnlyMethodhad no other callers outside its own module.No protocol schema changes:
sessions.patchparams andsessions.listsearch/archivedalready exist;pnpm protocol:checkis clean.User Impact
Android (and any non-admin operator client) can now rename, pin, archive, categorize, and mark sessions unread. Session lists page deterministically, and renames of channel-named sessions stick. Admin-gated session mutations are unchanged for admin clients.
Evidence
src/gateway/method-scopes.test.ts: write scope allowed for organization-field patches (incl.category/unread), denied for admin fields, mixed patches, and unknown keys; read scope denied; malformed params fall through to handler validation.src/gateway/server.sessions.store-rpc.test.ts: a client connected with["operator.read", "operator.write"]renames/pins/archives/categorizes viasessions.patch, filters archived lists, server-searches by the renamed label (assertingdisplayNamereflects the label over a stored channel name), and getsmissing scope: operator.adminforsendPolicy/mixed patches. Plus a tie-ordering/paging test.pnpm test src/gateway/method-scopes.test.ts src/gateway/server.sessions.store-rpc.test.ts src/gateway/session-utils.test.ts src/gateway/session-utils.search.test.ts src/gateway/sessions-patch.test.ts src/gateway/server.sessions.list-changed.test.ts src/agents/subagent-spawn.test.ts→ 20 files, 1200 tests passed (+ 25 in the spawn shard);pnpm tsgo:coreandpnpm tsgo:core:testgreen; oxlint/oxfmt clean on touched files.Client follow-ups (stacked): iOS/shared and Android PRs add server-backed session search UI on top of this.