| D-M2-02-1 |
2026-10-19 |
The task names the branch and the base: feat/M2-02-channel-behavior-config from int/m1-wave46 (9c05469); the work is neither merged locally nor pushed before its integration. The base holds the core contract in its earlier language; its entries channel.behavior.read, channel.behavior.update, agent.config.read, channel.settings_changed and agent.config_changed equal those of the authoritative contract in content, so no contract change is brought in. |
The task overrides the brief's branch and landing steps. |
| D-M2-02-2 |
2026-10-19 |
putChannelBehavior runs in one unit of work without a rights change: authorize first (unreadable 404, without manage 403 forbidden (reason capability_missing) before any step-up, stale step-up 401), then the space row FOR SHARE and the channel row FOR UPDATE, then If-Match against the locked settings_version (a weak, list or non-decimal value is a mismatch, 412 with current the behaviour at that version), then the field rules, then 409 conflict for an archived channel or a channel of an archived space, then the no-op check. |
Parent before child as setSpaceOperator locks (03 6.3); comparing after the lock makes two parallel PUTs with one If-Match end in one 200 and one 412, and the effective operator read after the locks is the one at commit. |
| D-M2-02-3 |
2026-10-19 |
Field rules, all failures in one 400 validation_failed: history_backfill_limit below zero is the schema code, above channel.history_backfill_limit_max too_large; a prompt above channel.prompt_max_bytes bytes of UTF-8 too_long (not 413), stored unchanged without trimming or character check except U+0000 (invalid_characters), which PostgreSQL text cannot store; a skill reference empty required, with a C0, C1 or bidi control or invalid UTF-8 invalid_characters (kernel/textcheck), repeated duplicate, each on /skills/<i>. No detail repeats the input. The limits are read at request time: a stored value above a later lowered maximum reads back unchanged, and a PUT that keeps it fails until it is lowered. No count or length limit of skill references beyond http.max_json_body_bytes (spec gap); a reference too large for the unique index answers too_long on /skills instead of 500 (red team of M2-02). |
03 leaves the codes open; the prompt is a multi-line instruction, not a display text of SEC-IN-4; the unique index (channel_id, skill_ref) makes a duplicate a field error. |
| D-M2-02-4 |
2026-10-19 |
Events: every effective change appends channel.settings_changed (receive action channel.read), and only a change of prompt or skills also agent.config_changed to the effective operator read after the locks (receive action agent.config.read), in this order, both with resource channel without version and the payload {channel_id, settings_version}. A channel without effective operator gets no agent.config_changed. One function, Service.settingsChanged, raises settings_version and appends them; M2-03 calls it for binding changes with the configuration flag set. |
05 6 and 09 4.4 name prompt, skills and project binding; a switch-only change reaches the operator through channel.settings_changed, which it receives by the operator rule. |
| D-M2-02-5 |
2026-10-19 |
Audit of channel.behavior.update: one allow entry per successful request with space_id, previous_settings_version, settings_version, changed (codes require_mention, thread_require_mention, free_response, auto_thread, allow_mentioned_agents, history_backfill_limit, prompt, skills in this order), the new switches and history_backfill_limit, and skills_count; a no-op entry has no_change: true and equal versions. Never the prompt or a skill reference. |
SEC-AUD-5: free text never enters audit details. |
| D-M2-02-6 |
2026-10-19 |
Scope serves agent.config.read: for an agent the channels that are not archived (nor in an archived space) whose effective operator it is and that it reads after the acting-for intersection, and their spaces; nothing fully visible, not instance-wide. Any other kind gets 403 forbidden with subject_kind_not_allowed. listServedChannels filters with both lists, orders by channel id, drops archived channels and checks every item in filter mode (a refusal is logged without ids and counted as filter_mismatch). The skills are read in the same statement as the channels. |
Lists count through Scope (02 9.3), and M1-28 allowed Scope only for read-level actions; one statement keeps a list item consistent with a concurrent change. |
| D-M2-02-7 |
2026-10-19 |
The admission rule of sync (Admit, the catch-up pages and the gateway hub) admits a personal event whose receive action is not self only to its audience and only while authorize allows that action in filter mode; personal events with self stay as they were. |
05 6 requires the receive action for every receiver; an agent that stopped operating the channel gets no agent.config_changed. |
| D-M2-02-8 |
2026-10-19 |
channel.settings_changed advances the structure generation of the delivery cache of authz like the structure events of 04 8.1 (spec gap reported for 04 8.1). |
The channel facts carry the mention switches that agent.invoke evaluates at delivery time (M2-08 on). |
| D-M2-02-9 |
2026-10-19 |
getServedChannelConfig and the list serve only the channel's effective operator; the fixed operator of a thread whose channel now has another operator reads nothing there (403 forbidden (reason subject_kind_not_allowed), spec gap reported). ChannelBehavior.project_binding is absent until M2-03. |
05 4.10 names "agent-self and effective operator"; the binding is M2-03. |
| D-M2-02-10 |
2026-10-19 |
docs/api/openapi.yaml gets none of the descriptions of the brief (the rules of putChannelBehavior, the list and config wording, the codes duplicate and invalid_characters in FieldError.code); this ruling, the module README and the problem bodies carry them. |
The base file is 18 bytes below repo.max_file_kb (D-M6-01-9); the gate must not be loosened, and splitting the contract is an owner decision. |
| D-M2-02-11 |
2026-10-19 |
The port chat.AgentConfig takes the actor as a parameter like chat.Structure, not from the context. The cross-check that channel.history_backfill_limit_default stays at most channel.history_backfill_limit_max is the existing check of chat/app.Config that fails the start in wire.NewChat; configchecks.go gets no copy. channel.prompt_max_bytes is registered with minimum 1. |
The brief follows the code where the merged M1 code names things differently; the start already refuses the combination. |
| D-M2-02-12 |
2026-10-19 |
The integration tests live in test/integration/behavior, a package on harness.Main, as four worlds with subtests named after the brief's tests. Sam reads Support/Allgemein through the group Leser (G-4), so his 404 cases use Support/Eskalation; the stale step-up comes from auth.step_up_max_age of the test server. The cases with personal tokens are not built. |
test/integration runs on pgtest.Main with its in-process world; a harness world per brief test would start twenty servers. |
| D-M2-02-13 |
2026-10-19 |
Canary: the class manage_only holds the prompt and skill canaries; it is allowed only in current of a 412 of putChannelBehavior (error records name their operation) and in success bodies, which no sink captures. Alex (reads Support, no manage) becomes a full observer with gateway, listEvents and getSnapshot; Lotse's listEvents pages from cursor zero are foreign records too. |
Brief note 14; the operator receives agent.config_changed but never the prompt. |
| D-M2-02-14 |
2026-10-21 |
Port onto main from the ticket branch (9c05469..eb37ba7, integrated as one squash on int/m1-wave48) after M1-69: the four operations move from later to enabled in api/operations.yaml; the console operations of the base (M6-01) are not on main and stay later, while the agent consumer operations of M2-07 are on main and stay enabled next to this ticket's (union of the enable list, the REST layer generated again); channel.prompt_max_bytes leaves tools/configdoc/planned.yaml; the English row T-AI-09 of the threat model names SEC-T10; the route table of api/routes/real_test.go, the chat README and the changelog take only this ticket's lines; the strict objects of api/rest grew above repo.max_file_kb, so the generator takes the cut of M6-01 unchanged (splitAll, halveObjects, as D-M4-31-5 and D-M3-03-28 do); the REST layer, the route table, config-schema.json and the configuration reference are generated again; comments name the English chapter files. INT-W48/1 (the canary readable set of Alex with the file scope) belongs to the pair M2-02 and M3-03 and lands with whichever of them comes second. |
The PR lane ports only the ticket; English documents on main win. |