| D-M4-28-0 |
2026-10-20 |
Base int/m1-wave50 at d200587, branch feat/M4-28-admin-agents in its own worktree with feat/M2-20-heartbeat-presence at cb2a50f and feat/M2-21-billing-switch-i4 at 721ad0e merged first (stacked build); not merged locally and no pull request from this build, the full verification runs on the build branch. The base holds every contract the brief needs (resolve_i1 on GrantCreate, GroupMemberAdd, OwnerAdd, InvariantViolation with organization_ready, AgentRuntimeStatus.warnings, D-W-100, D-W-114, D-W-116), so no contract commit is ported. The rows stand in a section of the ticket because the base has no section 10 in this file. Differences to the brief's names: the API has no listServedChannels; the operated targets come from Space.default_operator_agent_id and Channel.operator.effective_agent_id (listSpaces, listSpaceChannels); the brief's 10-backend-policy.md is 10-backend-richtlinie.md on the base; api_client has a generated model AgentPage, so a file importing both packages hides one of them. |
Instructions of the launcher, which override the brief's landing step; merged names win over the brief (D-W-100, D-W-114). |
| D-M4-28-1 |
2026-10-20 |
Presence as a parameter: AgentPage takes a Stream<AgentLiveState>; the desktop app maps the engine's EphemeralStore.watchPresence (state and reported mode, a value the store does not know is skipped), the console passes none and shows the presence of getAgent. The warnings always come from getAgent (presence.warnings), one text per code of 10 8 with its consequence; the screen never compares modes, so the badge gets no reported mode (DsBillingBadge would compare it). An adapter without a reported mode shows "Follows the configured mode per run". Place: lib/src/agents/agent_page.dart, agent_labels.dart, apps/client/lib/admin/admin_route.dart. |
Brief Design note 1; 15 1 principle 1. |
| D-M4-28-2 |
2026-10-20 |
Guided flow in three places: the grant preview offers "Switch to Organization and grant" for an i1_violation (with the sentence of 10 11 step 2 when organization_ready is false), asks a required reason and sends one createGrant with resolve_i1; adding a group member or an owner that the server answers with 409 i1_violation shows the same explanation and repeats that one call once with resolve_i1. The result shows every switched agent with the server's mismatch warning and the switch hint while the adapter still reports the old mode. A failure changes nothing (the server's transaction) and shows the problem. Place: lib/src/agents/guided_flow.dart, grants/preview_panel.dart, grants/grant_wizard.dart, groups/group_detail.dart, people/owners_section.dart. |
Brief Design note 2, fixed requirements 10 and 15; 05 rule 1.6; BR-07. |
| D-M4-28-3 |
2026-10-20 |
The proposed list of "All to Personal" comes from the newest agent.billing_mode.set_all to organization in the audit (listAuditEvents with action, newest first): the agents of agent_ids whose from_billing_modes entry is personal, and the holder when from_credential_holder_ids names one person. It is read only when the person picks personal, because audit.read has step-up; the list stays editable, and a refused audit read leaves the dialog without a proposal. One violating agent changes nothing (BR-08): the dialog stays open with the server's conflicts. Place: lib/src/agents/switch_all_dialog.dart, lib/src/data/agents_repository.dart. |
Brief Design note 3; 10 9.1; the audit details are those of the server's switched.attrs. |
| D-M4-28-4 |
2026-10-20 |
Token secrets are shown once in a code view with copy and are never stored: the secret lives only in the open view and is gone from the state when it closes. Creating asks for the step-up (token.create), revoking asks one confirmation and no step-up (token.revoke); the interceptor follows the server either way. Place: lib/src/agents/agent_tokens.dart. |
Brief Design note 4; 08 12, ADR 0061, 06 7. |
| D-M4-28-5 |
2026-10-20 |
An I1 rejection names only the principals the caller may read (the people and agents of its directory lists) and counts the others; the way back to personal is no guided flow, so the explanation ends with what the owner can revoke instead. Place: lib/src/agents/i1_explanation.dart. |
Fixed requirement 14; 10 5.4 and 11. |
| D-M4-28-6 |
2026-10-20 |
The fake server checks I1 after every rights change for every active personal agent and rolls everything back on a violation, like the server's VerifyInvariants; resolve_i1 switches the listed agents first and every row of the request shares one correlation_id. Simplifications: the default operator of a space operates all its channels, no thread operators, billing.mismatch_alert_after counts as one heartbeat (a switch alone sets only billing_mode_matches = false, as on the server), no tool_class_mismatch. Place: packages/sync_engine/lib/src/testing/rest/agent_handlers.dart, world.dart. |
Brief Scope (fake server handlers with BR-07 and BR-08); the README of sync_engine names the simplifications. |
| D-M4-28-7 |
2026-10-20 |
Step 5 of the guided flow and the switch hint of the agent page follow the server's comparison billing_mode_matches = false as well as the code billing_mode_mismatch: right after a switch the server reports the comparison at once and the code only after billing.mismatch_alert_after. The result shows the reported mode and every warning code of the server with its text. Place: lib/src/agents/guided_flow.dart, agent_page.dart. |
Red team finding (10 11 step 5 must show the adapter's state right after the switch; the comparison is the server's, so the client derives nothing). |
| D-M4-28-8 |
2026-10-20 |
The DPoP key thumbprint is a required field when creating an agent token, and the fake refuses an agent token without dpop_jkt with 400 validation_failed at /dpop_jkt. |
Red team finding: auth.dpop_required_for contains agent by default (08 12), so an optional field led into the server's refusal. |
| D-M4-28-9 |
2026-10-22 |
Port onto main: the ticket's own commits after the merges of M2-20 and M2-21 (both on main) land as one change; the changelog entry and these rulings are fragments (PL/24), both ARB files take the union of keys with main and the localizations are generated again; the fake's enrollment code keeps main's secure generator, and registerThreadViewHandlers stays next to the new registerAgentHandlers. Place: this change. |
Rules of the steering for porting from an integration branch; PL/24 forbids ticket sections in docs/DECISIONS.md and lines under "Unreleased". |