| M1-51/1 |
2026-10-13 |
Base int/m1-wave12 at da08f58, branch feat/M1-51-agents-records in its own worktree; the branch is not merged locally, the integration lane ports it. The migration takes the next free number of the base, 00016_agents.sql. Text this ticket adds is English, also in German chapters and READMEs. |
Instructions of the launcher, which override the brief's merge step and language (as M1-44/1). |
| M1-51/2 |
2026-10-13 |
adapter_kind matches ^[a-z0-9][a-z0-9_-]*$ (examples hermes, code_runner, custom); the use case checks it (400 validation_failed on /adapter_kind), the table has no check, because 03 lists none. There is no length key, so the request body limit bounds it. |
Ruling the brief asks for: a lower-case identifier like a handle, without dots, so it can be an audit code and a metric label. |
| M1-51/3 |
2026-10-13 |
Lock order of setAllAgentsBillingMode: permission_version first (rights hook, 03 section 6.3), then the agents rows in ascending principal_id, with agent_ids the listed rows (duplicates count once), without the list every personal row, disabled agents included. A first unit without rights change authorizes, checks the body and whether any agent differs from the target; when none does, it records no_change and the call ends without a rights version. A given but empty list switches nothing. |
One order for every multi-row change prevents deadlocks; the emergency switch must also reach agents that are disabled. |
| M1-51/4 |
2026-10-13 |
Disabling a disabled agent and enabling an active one answer 200 with the agent, without version, event or rights version, and audit no_change: true. The test line of the brief ("repeat gives 409") is not followed. |
The design notes, the acceptance criteria and 06 section 2 "No effect" say so; the test line contradicts all three. |
| M1-51/5 |
2026-10-13 |
A personal token of an owner gets 404 on the mutations of an agent and 403 on the two operations on the instance (createAgent, setAllAgentsBillingMode), not 403 everywhere as the brief's table says. |
SR5: the token has no owner role, so the agent is invisible to it (07 section 4.3 visibility before the requirement); authz decides, and invisible resources answer 404 like an unknown id (02 section 9.2). |
| M1-51/6 |
2026-10-13 |
TestBR01_CredentialHolderChangeIsChecked changes the holder of a personal agent of the sole owner to another active person; the owner keeps trigger everywhere, so I1 fails. A world with a second owner cannot hold a personal agent at all (03 section 6.4), so the brief's setup is not reachable through the API; a holder change succeeds for a disabled agent or to the sole owner. |
I1 without an owner exception (Ruling L22) makes the sole owner the only possible holder of an active personal agent. |
| M1-51/7 |
2026-10-13 |
TestCreateAgentHandleConflictCaseInsensitive creates the agent alex next to the person alex (409 conflict on /handle); Alex is refused with 400 by the pattern of AgentCreate.handle before any database access. The unique index on lower(handle) holds the case-insensitive rule, as M1-26 tests it. |
The API and the table accept lower-case handles only, so upper case can never reach the index. |
| M1-51/8 |
2026-10-13 |
Disabling and enabling bump agents.version and append agent.updated, like every change of the record or of the display name. |
status is part of the representation whose ETag is version; a client that holds the old version must see the new state. |
| M1-51/9 |
2026-10-13 |
listAgents first authorizes agent.read in filter mode on the descriptor {kind: agent} without an id: it answers the liveness of the caller (401 anonymous or disabled, 403 pending) and allows only an owner; not_found lets the list continue. Then candidates are read in id order in batches and each is filtered with authorize(p, agent.read, agent) in filter mode until one more than the page size is visible. |
07 section 7.4 rule 3; the descriptor check also proves the check of the route's action when the list is empty (02 "Proof of the check"). Filter mode keeps the per-item refusals out of the audit log. |
| M1-51/10 |
2026-10-13 |
presence.billing_mode_matches is always present: false only when a reported mode exists and differs from billing_mode, otherwise true. presence.rate_limit stays absent until the heartbeat stores it (M2-20). |
The brief's rule; no null (ADR 0053). |
| M1-51/11 |
2026-10-13 |
authz.AgentPolicySource stays as declared: the status comes from PrincipalSource, the effective tool class is the maximum authz computes from ToolClass and ReportedToolClass, and billing_modes_supported is part of the agent representation (Agents port), not of the source, because no rule of authz reads it. PersonalAgents filters by the principal status through identity.Directory; the agents adapter never reads principals. |
"Implement the interface as declared"; 03 principle 12. |
| M1-51/12 |
2026-10-13 |
A credential holder must be an active person: an unknown id, an agent, an app or a person who is not active is 400 validation_failed on /credential_holder_id (code not_active_person), checked again in the writing unit. setCredentialHolder on an organization agent is 409 conflict with reason billing_mode_not_personal. |
The brief; the holder's own subscription runs the agent (10 section 1). |
| M1-51/13 |
2026-10-13 |
The description is a display text under SEC-IN-4 (no control or bidi characters, may be empty); the reason of every policy, holder, tool class, disable and enable call must not be blank (400 on /reason), also for ToolClassSet, whose schema has no minLength. |
One rule for every administrative reason (SEC-AUD-5), as identity checks it. |
| M1-51/14 |
2026-10-13 |
createAgent generates the agent id before its writing unit and declares it as the affected principal; identity.PrincipalWriter.CreatePrincipal accepts the id. 412 version_conflict carries the full agent as current. |
The rights hook records the views before the change, so the principal must be named first (M1-29). |
| M1-51/15 |
2026-10-13 |
Audit details: from_billing_mode, to_billing_mode, from_credential_holder_id, to_credential_holder_id (only where there is one), from_tool_class, to_tool_class, changed_fields, from_status, revoked_tokens, adapter_kind and reason; setAllAgentsBillingMode writes one entry with agent_ids and the parallel from_billing_modes, held_agent_ids with the parallel from_credential_holder_ids, to_billing_mode, to_credential_holder_id and reason. |
10 section 14 with the flat attributes of audit_details.v1, whose lists hold one kind; ids and codes only, never names or configuration values. |
| M1-51/16 |
2026-10-13 |
architecture/modules.yaml grants agents the adapter vendor pgx. The integration tests use the real API tokens of identity for agents (so a revocation takes effect in api_tokens) and fake sessions for people. |
The repository and the sqlc code need the driver; step-up and sessions are not under test here. |
| M1-51/17 |
2026-10-15 |
Port onto main: code and tests are taken byte for byte from the integration squash, including the test worlds of INT-W14/8. The migration is 00022_agents.sql (INT-W14/2), not the 00016_agents.sql of M1-51/1; its comments, the code comments and the module README name the English chapter files and headings (03-data-model.md, 05-actions.md, 07-permissions.md, 09-agents.md, 02 "Source port" and "Proof of the check", 06 "No effect", 03 principle 2, 7, 10 and 12), so its checksum is recorded anew. The main-only case TestEmptySourcesKnowNothing of M1-29 no longer checks emptyPolicies, which this ticket removes because agents is now the agent policy source. Generated files are regenerated, not merged. |
main is English; the case was added on main after the integration base. No behaviour changes. |