Skip to content

M1-54 Operators with invariant I1

No Date Ruling Reason
M1-54/0 2026-10-14 Base int/m1-wave16 at 1e260ad, branch feat/M1-54-chat-operators in its own worktree; the branch is not merged locally, the integration lane ports it. Text this ticket adds is English. The port methods take the caller as a actor.Actor like every structure method (M1-53/0); the commands carry target, assignment, reason and If-Match. Instructions of the launcher, which override the brief's merge step and language.
M1-54/1 2026-10-14 Order of checks (D2): load, authorize (unknown and invisible 404, non-owner with read 403 forbidden with reason: owner_only, stale step-up 401), If-Match (428, 412 with current), the body of D1, then 409 conflict for an archived target (an archived space freezes its channels). A request without If-Match is answered 428 by the request chain before any handler, as for every operation with a required If-Match; this leaks nothing, because unknown and foreign targets get the same answer. Like archiveSpace the use case runs two units: a first one decides and records the same assignment as a call without effect (200, no version, no event, no permission_version, allow entry with no_change); only a real change runs the second unit, declared as the rights change with the target as resource, which locks permission_version first (03 6.3), then the target row and, for a space, its inheriting channels in ascending id, and checks If-Match and the archive state again under the lock. A personal API token of an owner acts with the grants of the person without the owner role (SR5): without a grant that reads the target it is 404, with one 403. 06 "No effect" and 03 6.3; a no-op must not raise the rights version; SR5 decides the token case, D2 names its 403 for a token that sees the target.
M1-54/2 2026-10-14 Served targets (D5): chat implements ServedTargets in adapters/authzsource with the query of 10 5.3 (queries/operators.sql): channels that are not archived in spaces that are not archived whose effective operator is the agent, channels with a thread of kind thread whose fixed operator is the agent (open or closed), and every space that is not archived whose default operator is the agent as an entry with a zero channel (D5 point 4). The I1 query of authz already accepted such space entries (M1-29), so internal/authz is unchanged. A channel of an archived space never counts, as in 10 5.3. Without point 4 a grant of trigger on an empty space with a personal default operator would pass, and a later createChannel (no I1 action) would break I1; TestI1_SpaceDefaultOperatorServesSpace holds it.
M1-54/3 2026-10-14 OperatorSet accepts no resolve_i1 before M2-21, although 05 rule 1.6 names it for both operator actions: the request decoder refuses the unknown member with 400 validation_failed. The guided switch to organization is the M2 feature of M2-21 (D8 of the brief).
M1-54/4 2026-10-14 Affected principals (D3): the second unit names the old and the new effective operator of the channel (for a space: the old and the new default) through authz.RightsChanges.AddAffected after the row locks and before the write, so the set is exact under the lock of permission_version; the rights hook verifies I1 for every personal agent (M1-29) and writes access.changed. chat/app.Deps takes the rights hook, the principal directory of identity and the agent policy source (an agent_id is valid when the principal is an active agent and has an agents row); wire.ChatWrites gains Rights, and wire.NewChat builds the directory and the policy source over the pool. 02 8.1 and 10 5.1; chat may use the ports of identity and authz (02 4.4).
M1-54/5 2026-10-14 The structure source answers Thread (channel, kind, fixed operator, creator) from this ticket on, replacing ruling M1-53/6, because authorize resolves message.create and the operator rule of a thread through it (D6; AZ-M29 needs the main thread). wire keeps only the empty dm part. Thread creation stays with M1-55: TestBR_02_ClosedThreadWithFixedPersonalOperator inserts its closed thread as a SQL fixture, because the chat repository has no thread insert besides the main thread yet. D6 of the brief; the brief's "through the chat repository" needs a use case that M1-55 builds.
M1-54/6 2026-10-14 Test world (test/integration): the world of 07 sections 10 and 11.1 through the API with the real identity, authz, agents, chat, audit and sync modules and the production sources; only the login is the test authenticator. The grant G-6 of the app Tracker is left out (apps come with M2). The via_agent case of the explanation uses the organization agent Texter (07 10.2, hint H1), not a personal agent as the brief says: a personal agent is triggered only by its holder, an owner who reads every channel, so it can never produce via_agent. The brief asks for the world of 07 with real sources; the via_agent semantics of M1-49 (07 section 6) decide the agent.
M1-54/7 2026-10-14 Files outside the scope list of the brief: internal/wire (chat.go, empty_sources.go, serve.go) to bind the new collaborators and drop the empty thread and served-target part, and api/routes/real_test.go (the stub server and the table of enabled operations), which every newly enabled operation must extend. Without them the operations and the sources are not wired, and the routes tests do not compile.
M1-54/8 2026-10-15 Port onto main: code and tests are taken byte for byte from the integration squash, apart from the comments named below. The integration rulings INT-W17/1 to INT-W17/5 resolve the merge with M1-55 (the shared Thread method, one Deps.Policies, the split of rest.gen.go, the person test helper) and belong to the port of M1-55, so none of them is folded in here; without M1-55, rest.gen.go stays below repo.max_file_kb. The integration base also holds M2-39 and M2-40 (terminal), which are not on main, so nothing of them is ported: setHostState stays out of the enabled list of api/operations.yaml, the host route rows stay out of api/routes/real_test.go, and the later entries removed for setSpaceOperator and setChannelOperator use the key milestone of main. The ticket adds no migration. The comments of queries/operators.sql, adapters/authzsource and app/operators.go name the English chapter files and headings (03-data-model.md, 10-backend-policy.md, 02-architecture.md "Ports, events and sources per module", 03 principle 12, 06 "No effect"), and the comment of the empty sources in wire keeps the reference of main to ruling D2 of the M1-28 brief. The changelog entry, the module README (status line, code list and the new section on operators) and these rulings are written into the English text at the same places. Generated files are regenerated, not merged. main is English and does not hold M1-55, M2-39 or M2-40.