Skip to content

M1-29 Rights changes, access.changed and invariants I1 and SR8

No Date Ruling Reason
M1-29/1 2026-10-12 access.changed is written only for an affected principal whose difference is non-empty; a change without visible effect still raises the rights version. Ruling of the brief: later events carry the version in the envelope; an empty event would only make clients reload.
M1-29/2 2026-10-12 I1 is checked for all active personal agents on every rights change. Ruling of the brief: 10 section 5.1 names the minimum per action; the superset cannot miss a case, and 03 section 6.4 already asks the check for shrinking changes.
M1-29/3 2026-10-12 The declaration of the brief is txn.RightsChangeSpec with the option txn.DeclareRightsChange; action and resource kinds are strings and txn.Ref names resources and targets. txn.RightsChange(principals...) stays and adds up with it; Options.Covers also requires the targets and the action of a nested unit. RightsChangeOf and InRightsChange read the declaration. RightsChange is already the name of the option, and kernel ports never import each other (go-arch-lint: kernel/txn may not import kernel/authorize).
M1-29/4 2026-10-12 The hooks of phase 1 and 3.1 are the method values Begin and BeforeCommit of authzapp.RightsHook, which also implements authz.RightsChanges.AddAffected and VerifyInvariants. The appender of sync and the recorder of audit are bound after the unit of work exists (Bind, start step authz); an unbound hook fails every rights change with kind internal. The recorder writes through the unit of work whose registry holds the hook, so it cannot exist when the registry is sealed; failing closed keeps a rights change from committing without its event and audit.
M1-29/5 2026-10-12 The deny entry of a violation goes to Recorder.RecordDenial, which aggregates per audit.deny_aggregate_window and writes in its own transaction after the rollback. last_owner wins over i1_violation when both hold. Brief "Deny audit entry"; without an owner no personal agent can be in order either, and AZ-M26 expects last_owner.
M1-29/6 2026-10-12 The view of a principal (the input of access.changed) counts a person as in a session: the owner role takes effect for an active owner. dm channels are not part of it; a space is visible with read on it or on one of its channels. The input type of DiffVisible is domain.AccessView. The view is what the person sees in the app; rights changes never change dm participation; domain.Snapshot is the rights snapshot of M1-28 (merged names win).
M1-29/7 2026-10-12 For declared targets authz derives the affected principals from the grants in effect on the instance, the target, its space and, for a space, its channels, with group members, the active owners and the effective operators of the channels. Fixed thread operators are not added. The difference step drops everyone without a change, so the superset is safe; the view of an agent on a channel never depends on a thread it operates.
M1-29/8 2026-10-12 StructureSource.ServedTargets returns authz.ChannelRef{ChannelID, SpaceID}, with a zero ChannelID for a space whose default operator is the agent; StructureSource.SpaceArchived and ChannelFacts.Archived (channel or space archived) are added. Effective caps every level at read on an archived channel, owners included, and Decide answers a requirement above read there with channel_archived (07 section 4.5 point 1). chat fills them from M1-52 to M1-54. 03 section 6.4 point 3 needs spaces without a channel; test 10a of the brief caps a channel at read through the target path, and the cap is rights logic, which lives only in authz (B7). grant.create must refuse archived spaces.
M1-29/9 2026-10-12 AgentPolicySource.PersonalAgents returns authz.PersonalAgent{AgentID, CredentialHolderID}. authz.AgentPolicy already exists (M1-28) with other fields.
M1-29/10 2026-10-12 The store refuses writes of memberships and grants and SoftDeleteGroup outside a unit declared as a rights change; InsertGroup and the new UpdateGroup are no rights change. New store reads: ActiveTriggerGrants, ActiveGrantsOnTargets, ActiveMembershipsOfGroups, MembershipsOfUserInGroup. The store doc of M1-27 left the enforcement to M1-29; group.create and group.update have "Permissions" no in 05.
M1-29/11 2026-10-12 Inside a unit declared as a rights change the Authorizer neither reads nor fills the snapshot cache, the version memo or the scope memo. The unit sees its own uncommitted version; an entry cached under it would survive a rollback and could serve another change that commits the same number.
M1-29/12 2026-10-12 grant.revoke resolves a revoked grant like the stored grant (visibility and the delegation guard decide); every other action still finds no revoked grant. 06 section 2 "No effect": revoking a revoked grant succeeds without change, which not_found would contradict.
M1-29/13 2026-10-12 Admin calls that may be without effect (DeleteGroup, RemoveGroupMember, RevokeGrant) decide that in a first unit without rights change and run the change in a second unit declared as one, which checks everything again under the lock. When that second unit finds the call without effect after all (a concurrent call did the change first), it rolls back, so no rights version is raised, and the first unit runs again to record the call without effect. An UpdateGroup without change audits no_change: true too. No rights version for a call without effect (R-API-27), while the change itself still runs fully under the lock of permission_version.
M1-29/14 2026-10-12 AddGroupMember closes a membership that is not removed but expired (removed_at, removed_by) before inserting the new one; a membership in effect is 409 conflict. Claim groups: rename, add and remove are 409 conflict with reason claim_managed_group; the description may change. The unique index allows one row without removed_at per person and group; 07 section 2.6 names the reason for claim groups.
M1-29/15 2026-10-12 CreateGrant answers a host target with 404 until M2-39 brings hosts; an archived space or channel is 409 conflict with reason channel_archived, checked after authorize so an invisible target stays 404. An optional revoke or delete reason is checked with SEC-IN-4 and audited as ReasonText. No host can exist before M2-39; the order keeps 02 section 9.2.
M1-29/16 2026-10-12 AdminService takes the caller as actor.Actor and puts it into the context for the recorder and the events; a different principal already in the context is an internal error. Group text limits come through AdminDeps.Limits; the keys group.name_max_chars and group.description_max_chars are bound when M1-48 wires the service with its routes. The recorder reads the actor from the context only (M1-24); the brief adds no configuration key.
M1-29/17 2026-10-12 errs.ProblemExtender members are merged by the problem writer in name order after the standard members; a standard name (also an absent one) is never taken, nil members and nested nulls are dropped, an internal error carries none. 10 section 5.4 and ADR 0053.
M1-29/18 2026-10-12 The integration tests use PostgreSQL 18 with the real identity PrincipalSource, the real unit of work, sync and audit, and the fakes of structure, agent policy and credentials. Owner, status and operator changes of later tickets are test use cases that change SQL rows or the fakes inside the transaction and declare the action. A personal token of the owner gets owner_only for owner actions (SR5), not step_up_not_possible. The brief's test setup; the requirement step decides before the step-up step (M1-28).
M1-29/19 2026-10-12 Base int/m1-wave6 at 785916c, branch feat/M1-29-authz-invariants in its own worktree; the branch is not merged locally and the worktree stays, the PR lane ports it. Text this ticket adds is English. Instructions of the launcher, which override the brief's merge step and the brief's language.
M1-29/20 2026-10-12 Red team round 1 (verdict JA, four minors): the raced call without effect no longer raises the rights version (M1-29/13, TestRevokeGrant_RacedRevocationIsWithoutEffect); every view of one change is computed at the time of Begin, so a grant that expires during the unit is reported by the expiry sweep and not as part of this change, while VerifyInvariants reads the clock at the check; the example of txn.UnitOfWork.Do declares with DeclareRightsChange. The comment of identity/app.Store.InsertOwner, which still names txn.RightsChange, stays for M1-31, because the brief allows only the adapter method in internal/identity. Closing an expired membership with the caller as removed_by stays (M1-29/14). Findings of the red team fixed in the same ticket where they are in scope.
M1-29/21 2026-10-13 Port onto main: the start step sync runs before authz (6b sync, 6c authz), and TestAZ_P7_CoversRule16 reads rule 6 of docs/spec/05-actions.md from "I1 is checked transactionally on" to "check in [03-data-model.md]". startAuthz binds the rights hooks to the appender of sync and fails without it; main had built authz first because nothing needed the event log there before. The chapter is English on main.