Skip to content

M2-03 Project binding and readiness

No Date Ruling Reason
D-M2-03-1 2026-10-20 Schema project_runner_config.v1 is defined in 09 section 18.1 and as ProjectRunnerConfig in docs/api/openapi.yaml: a closed object with the optional members model (string, minLength: 1) and allowed_tools (array of strings with minLength: 1, uniqueItems: true). The server checks only this shape and the rules of D-M2-03-3 and never interprets a tool name; the runner derives its flags from agents.tool_class and only narrows them by the list, so it enforces "within agents.tool_class" of 03. The stored jsonb keeps the members as sent (an empty tool list stays an empty list). 03 and the contract named the schema, no file defined it; W7 of M1-18 requires closed request objects.
D-M2-03-2 2026-10-20 Configuration key project.binding_text_max_chars (Server, default 512, minimum 1, maximum 600, chapter 09): the longest workspace_ref, repository_url, default_branch, runner_config.model and tool name in Unicode characters, read at request time; longer gives too_long. 512 characters of at most 4 bytes in UTF-8, also after lower(), stay below the PostgreSQL B-tree entry limit of the unique index on lower(workspace_ref); the maximum keeps that true for every configured value, and an index error that still occurs answers 400 validation_failed with too_long on /workspace_ref instead of 500. The reason uses audit.reason_max_chars. Without a limit an over-long reference reaches the index and ends as an internal error.
D-M2-03-3 2026-10-20 Field rules, all failures in one 400 validation_failed, per text first invalid_characters (kernel/textcheck), then too_long, then invalid_format: workspace_ref blank after trimming is required; leading or trailing white space, a leading /, \, ~ or -, a drive letter with colon or a segment .. between / and \ is invalid_format; repository_url must be an absolute http or https URL with a host and without user information; default_branch must not be empty, contain white space or .., or start with -; model and each tool must not be empty or start with -; the reason blank is required. 03 "never an absolute path of the server", 03 principle 9 (no credentials), SEC-OUT-1, the runner passes model and tools as process arguments (09 18.2); defense in depth beside the confinement under runner.projects_root.
D-M2-03-4 2026-10-20 A PUT replaces the whole binding: an absent optional member clears its column and an absent runner_config is {}; created_by and created_at stay. Equal values (the runner configuration compared as jsonb, so the order of allowed_tools and the presence of an empty list count) are a no-op: 200 with the stored binding and version, no write, no event, ready untouched, and an audit allow entry with op = unchanged. HTTP PUT semantics; a client that retries a lost PUT must not reset readiness.
D-M2-03-5 2026-10-20 Every effective PUT change (op = replaced) writes the new values with version + 1 and ready = false; every change of the effective operator of a bound forum (setChannelOperator, setSpaceOperator for its inheriting forums that are not archived) sets ready = false with version + 1 in the same transaction, without an extra event. The audit detail ready_reset is true when a ready binding was reset by the PUT. The runner reported readiness for the old configuration or as the old operator; 05 section 5 point 7 must fail closed until the serving runner reports again.
D-M2-03-6 2026-10-20 ApplyProjectReadiness applies a report only for a bound forum that is not archived (nor in an archived space) whose effective operator, read after the locks, is the reporting agent; every other entry is ignored without error. A channel reported twice is validation_failed with code duplicate on /project_readiness and changes nothing. A readiness change raises only project_bindings.version; it writes no event, no audit entry and no settings_version. Locks: the spaces of inheriting reported channels FOR SHARE, the channels FOR SHARE, the bindings FOR UPDATE, each in ascending id. 09 4.8 "runner only"; the result tells an agent nothing about forums it does not serve; readiness is runtime state, a settings_version change on each flip would fail pending putChannelBehavior calls with 412.
D-M2-03-7 2026-10-20 Status codes: a channel that is not a forum is 400 validation_failed with not_a_forum on #path/channel_id (PUT and DELETE, after authorize); an archived forum or a forum of an archived space 409 conflict; DELETE without a binding 404 not_found without an audit entry; If-Match without a binding 412 version_conflict without current, a stale or malformed If-Match 412 with the stored binding as current; a workspace_ref taken case-insensitively by another forum 409 conflict without reason. 06 5 point 3 (pointer form), 07 4.5, 06 7 (no tombstone), RFC 9110 (no current representation), 03 unique lower(workspace_ref).
D-M2-03-8 2026-10-20 A binding change appends channel.settings_changed and, when the forum has an effective operator after the locks, agent.config_changed to it, through Service.settingsChanged of M2-02 with the configuration flag set; without an effective operator only channel.settings_changed. No receiver exists; M2-02 handles this case the same way (D-M2-02-4).
D-M2-03-9 2026-10-20 docs/api/openapi.yaml stays below repo.max_file_kb: the schemas ProjectBinding and ProjectBindingPut and the responses and headers of both binding operations are written in YAML flow style, which frees the bytes for ProjectRunnerConfig, minLength: 1 on the reason and 409 on deleteProjectBinding; the descriptions of the rules of design notes 3, 5 and 6 of the brief are not added, this table and the module README carry them. The base file was 18 bytes below the limit (D-M2-02-10); the closed schema and the 409 are needed by the request and response validators, the descriptions are not.
D-M2-03-10 2026-10-20 The port follows the brief where it differs from M2-02: chat.ProjectBindings takes the caller from kernel/actor in the context. PutProjectBindingCmd.IfMatch is the existing chat.Precondition instead of ExpectedVersion *int64, so a malformed header is a 412 like a stale one. The handlers live on chathttp.Behavior next to the behaviour operations. The brief fixes the context form; Precondition already carries the parsed If-Match of every other chat operation.
D-M2-03-11 2026-10-20 agent.invoke reads ProjectBound and ProjectReady from the LEFT JOIN on project_bindings of every channel query of the structure source; inside a structure scope (one per dispatched event of the hub) the guard reads the project facts of the channel again instead of the memo. Nothing raises permission_version or writes access.changed. Readiness changes write no event that would end a cached state (07 SR2); the REST mode reads facts per call anyway.
D-M2-03-12 2026-10-20 The task names the branch and the base: feat/M2-03-project-binding from int/m1-wave46 (9c05469) with feat/M2-02-channel-behavior-config (eb37ba7) merged first; the work is neither merged locally nor opened as a pull request, the launcher integrates it. The base holds the core contract in its earlier language; the rules the brief cites (R-AZ-01, R-AZ-03, R-AZ-04, R-AZ-09, R-AZ-10, R-AZ-14, 05 section 5 point 7) say the same there, so no contract change is brought in. The integration tests live in test/integration on its in-process world (the port ApplyProjectReadiness has no route until M2-20); the world keeps its chat service and unit of work and serves the behaviour handlers. The launcher's task overrides the brief's branch and landing steps.
D-M2-03-13 2026-10-21 Port onto main from the ticket branch (ce35cae..8b880d3) after M2-02: the migration becomes 00048_chat_project_bindings.sql, the next free number after 00047 on main (the integration branch had renumbered it to 00050, INT-W49/1); the spec text of 09 sections 18.1 and 21 goes into the English 09-agents.md; the chat README and the changelog take only this ticket's lines; comments and config-schema.json name the English chapter files; the sqlc models, the REST layer, the API clients, the configuration reference and the contract copy of the fake server are generated again against main. D-M2-03-12 no longer applies: the ticket lands as its own pull request. The PR lane ports only the ticket; English documents on main win.