Repository navigation
Description
Amended 2026-10-02 (#852, PR #1021): the name-resolution contract exists as
IFlockLookup.ResolveByNameAsync(found / not found / ambiguous with candidates). See this comment.
Slice 6 of #789. Blocked on #788, #787, and slice 2 (#805). This is the only write in the phase-1 surface.
Why #787 is a genuine prerequisite, not defence-in-depth
RecordDailyEntry consults FlockScopeGuard. Its first line is if (!user.IsResolved) return Result.Success();, which is fail-open.
Slice 2's McpCallContext is the primary control, but it bounds the hazard rather than closing it. A helper opening a secondary DI scope leaves CurrentUserContext unresolved in that scope, which lands exactly on that branch. #787 flips it fail-closed, turning a silent grant into a refusal.
Note the asymmetry: #787 backstops the write path only. A secondary-scope read never calls FlockScopeGuard. It goes through the EF query filter, which reads FlockScope directly, and an unresolved FlockScope is unrestricted. That read residue is recorded as open risk.
Scope
- Takes a flock name-or-id and grade names-or-ids. Derives
FarmId/HouseIdfrom the flock aggregate. The caller's surface drops from four unguessable GUIDs to one flock and some numbers. One of those GUIDs,HouseId, is a phantom id until Phase 2's House model. - Calls the unchanged
RecordDailyEntryValidatorandRecordDailyEntryHandler. No parallel validation path. - Required idempotency key, keyed on the tool name so two tools cannot collide.
- Name resolution means exactly one accessible match, because duplicate flock names are legal.
Guards worth naming
- Guard 7b (dependency-graph walk plus the causal second-scope test) is the primary control. It has two mutations: direct
IServiceScopeFactoryinjection, and a factory-registered helper hiding it behind a delegate. A constructor walk sees nothing in the second case. - Replay must assert the absence of a second
DailyEntryUpdateaudit row, NOT the entry count. The handler upserts on(account, farm, house, flock, date), so a count assertion passes with idempotency entirely removed. - A Worker not assigned to the flock must be refused.
Design
docs/plans/770-mcp-server/02-guards.md rows 7b, 21-23, 25-27, 32, plus the honesty note on what these guards do not close.
Activity
mforce commentedon Sep 14, 2026
Before you pick this up: the name-resolution policy this slice needs does not exist yet
Found on 2026-09-14 while re-planning #514 (modular monolith), verified against
main@6231b31.No defect is claimed in shipped code. This is a premise in the design that will cost this slice
time if someone discovers it at review instead of at planning.
docs/plans/770-mcp-server/01-design.md:209says the tool layer "deliberately does not contain:any validation rule, any authorization decision of its own, any tenant or flock-scope predicate.
Each already exists one layer down and is already guarded."
For flock name resolution that premise is false.
IFlockRepository(
src/Cluckwork.Application/Features/Flocks/IFlockRepository.cs) exposesListAsync,SearchAsync,GetDisplayNamesAsyncandGetByIdForFlockScopedWriteAsync. There is no name lookup. The onlyname-to-flock resolution anywhere in
src/isSimulationDataSeeder.cs:796:That is exactly the take-the-first-match behaviour guard 32 forbids and adversarial review finding 3
identified. So this slice has to invent the ambiguity contract, and the design's own rule says it must
not live in
Mcp/.The two halves are not symmetric
cluckwork_record_daily_entryresolves a flock name-or-id and grade names-or-ids(
01-design.md:194-195). Only one half is dangerous:IX_EggGrades_AccountId_FarmId_LowerNameis a unique index onlower(Name)per account+farm (docs/schema/public.EggGrades.md:51)IEggGradeRepository.NameExistsAsyncshows the code already relies on the uniqueness. FilteringListActiveAsyncis safe, because ambiguity cannot arise.FlockRepositorysays so outright and itsThenBy(f => f.Id)tie-break exists because of itSuggested placement
Put resolution behind a Flocks-owned read contract, a
ResolveAsyncreturning found /not-found / ambiguous-with-candidates, rather than inside the tool. Three reasons beyond the
design's own rule:
SimulationDataSeeder.cs:796is an existing caller that should stop taking the first match.#514's re-plan carries this as a candidate first contract, but this slice is not blocked. It can
define the contract itself. The note exists so the placement is a decision rather than a discovery.
mforce commentedon Oct 2, 2026
Amendment from #852 (PR #1021). The name-resolution contract this tool needs now exists, so
Mcp/does not have to author that policy.IFlockLookup.ResolveByNameAsync(name, ct)returnsFlockNameResolution.Found(FlockReference),NotFound, orAmbiguous(candidates ordered by id).Flocksset. The tenant and flock-scope filters (Add discovery guard for flock-scoped EF query filters #613) decide the candidates, so a Worker sees only assigned flocks.Found. Eligibility is a separate check,FlockDetails.CanRecordProductionOn(date)fromIFlockLookup.GetForFlockScopedWriteAsync.FlockNameResolutionTestscovers tenant isolation, Worker scope and the candidate order.Note the read-path residue this issue already records. An unresolved
FlockScopeis unrestricted, so a secondary DI scope would see every flock in the farm. Resolve by name in the request scope.mforce commentedon Oct 6, 2026
Updated in the 2026-10-06 re-evaluation. Still L.
FlockScopeGuardrefuses an unresolved actor before reading assignments, so the fail-open line this body quotes is gone and the blocker is met. The read residue is unchanged.RecordDailyEntryroute with a tool-namespacedIdempotency-Key. Extract Make API write idempotency atomic across replicas #307 into a shared unit only if loopback loses on evidence. Either path keeps MCP slice 1: unblock /mcp and extract the idempotency protocol #804's rule: the Make API write idempotency atomic across replicas #307 suites pass unedited. Offline data capture (PWA) — tech spec KD-4/§6 #50 (offline PWA) relies on the same idempotency code.RecordDailyEntryHandlerorRecordDailyEntryValidatorfromCluckwork.Api.Mcpis a CW1004 build error. UseIEggOperationsModule.RecordDailyEntryAsyncand injectIValidator<RecordDailyEntryCommand>, asDailyEntryEndpoints.csdoes. Guard 25 should be restated in those terms.DiscardedEggs, which the design's sample call omits.FarmIdandHouseIdcome fromFlockDetails.feat(web): neutral write-permission copy, the 1,950 KiB precache ceil…
mforce commentedon Oct 10, 2026
Amendment: OAuth is available, but the loopback option needs an authentication design. This adds to the October 6 amendment. Its contract, name-resolution and idempotency requirements still apply. Source links pin main at
bd4e5eca.PR #1136 shipped OAuth request checks and scope gating. PR #1144 shipped consent for
daily-entries:write. PR #1138 shipped connected-app audit attribution, and PR #1147 added the farm switch. Seesrc/Cluckwork.Api/Modules/Access/OAuth/OAuthEndpoints.cs:65,src/Cluckwork.Application/Modules/Access/Contracts/OAuthScopes.cs:5andsrc/Cluckwork.Infrastructure/Repositories/AuditWriter.cs:53.Forwarding an OAuth bearer to the existing REST routes fails. Today's scheme selector sends ordinary REST routes to session-JWT authentication:
src/Cluckwork.Api/Hosting/CluckworkIdentityServiceCollectionExtensions.cs:185. The record-daily-entry route has no OAuth opt-in:src/Cluckwork.Api/Modules/EggOperations/DailyEntries/DailyEntryEndpoints.cs:16. It therefore returns 401 for that bearer. AddingAcceptOAuthTokensalone would make the route OAuth-only and reject the SPA's session JWTs.Price loopback with that constraint included. Choose and record either an HTTP boundary with explicit authentication and scope rules, or a shared idempotency unit around the module-contract call. Preserve the existing #307 suites.
The write tool, its authentication/idempotency design and its acceptance tests remain unbuilt. Prove role and scope restrictions, Worker flock access, farm-switch refusal, app audit attribution and replay without a second
DailyEntryUpdateaudit row. The tool records a draft; a person still submits it. Milestone 10 deliberately provides neither this tool nor OAuth access to the ordinary REST route.mforce commentedon Oct 10, 2026
Re-check after milestone 10, 2026-10-10.
IEggOperationsModule.RecordDailyEntryAsyncneeds neither. Record the choice in this slice.daily-entries:writeas well asProductionWrite.daily-entries:writedoes not implyfarm:read. Name resolution must therefore not depend on a read tool.IFlockLookup.ResolveByNameAsyncinside this tool covers it.AuditWriter.cs:58). Assert it on the recorded entry's audit row.mforce commentedon Oct 10, 2026
Decision recorded on #789, 2026-10-10: loopback is dropped. The write tool calls
IEggOperationsModule.RecordDailyEntryAsyncthrough a shared idempotency unit. The #307 suites still pass unedited, and replay must not write a secondDailyEntryUpdateaudit row.test(mcp): assert the audit rows first in the replay test
fix(mcp): re-check flock access before returning a replayed entry
test(mcp): guard 21 refuses an idempotencyKey with a default value
test(api): a failed response write after commit replays on the same-k…
Merge origin/main (#1209 read tools) into feat/809-mcp-record-daily-e…