Repository navigation
Tie an investigation claim's later calls to the named token that made it - #6738
Conversation
Submit, renew and evidence now refuse a named token that presents another token's claim_id (claim_not_token), journaled under the caller. A legacy-token claim whose agent_id is a served named agent token's name is refused.
| && !string.Equals(holder, tokenName, StringComparison.Ordinal)) | ||
| { | ||
| await store.RecordRefusalAsync(requestId, tokenName, "claim", ClaimNotToken, null, cancellationToken); | ||
| return McpHelpers.Refusal("claim_id", ClaimNotTokenMessage(tokenName)); |
There was a problem hiding this comment.
The renew arm does not answer claim_not_token — it answers status: "invalid".
McpHelpers.Refusal is Status("invalid", sentence, new { parameter }) (PerformanceMonitor.Common/Mcp/McpHelpers.cs:946-949), so the only place claim_not_token appears on this path is the journal reason (claim:claim_not_token, line 439). Submit (line 681) and evidence (line 555) return InvestigationToolCore.Status(ClaimNotToken, …), so the same violation answers with two different statuses depending on which tool the caller reached.
Three places say otherwise:
- The PR body: "Submit, renew and evidence … the call is refused with the new status
claim_not_token". - The CHANGELOG entry, which ships: "submitting a report, renewing a claim or reading evidence with another token's claim_id is refused with claim_not_token".
claim_investigation_request's own description (line 362-374) is untouched by this PR: its status roster is still "claimed, renewed, already_claimed, not_open, not_found or unsupported_format" and says nothing about eitherinvalidorclaim_not_token.submit_investigation_report's roster was updated in this same diff (line 585), so an agent can discover the refusal there and not here.
The test encodes the actual behaviour (Assert.Equal("invalid", StatusOf(refused)), InvestigationClaimTokenLiveTests.cs:162), so this is the body and the changelog overstating the code, not the test being wrong.
Either make the three arms uniform:
| return McpHelpers.Refusal("claim_id", ClaimNotTokenMessage(tokenName)); | |
| return InvestigationToolCore.Status(ClaimNotToken, ClaimNotTokenMessage(tokenName)); |
(which is what the changelog already promises, and what an agent keying retry logic off status needs), or keep Refusal for symmetry with the sibling AgentIdNotToken guard above and instead correct the body/changelog and add the refusal to the claim tool's description. What should not ship is the current three-way disagreement.
| "after a later evidence_ready. sections (optional) names the sections to return; omit it for all. A request that " + | ||
| "another agent holds a live claim on is refused (already_claimed) unless you pass its claim_id; an unclaimed or " + | ||
| "finished request is served to any caller.")] | ||
| "finished request is served to any caller. A named agent token that passes another token's claim_id is refused (claim_not_token).")] |
There was a problem hiding this comment.
The guide sentence lands here, but get_investigation_evidence's status roster three lines up (line 515) is unchanged: "status ok, empty, not_found or already_claimed". EvidenceAsync now returns a fourth, claim_not_token (line 555), and the roster is the sentence an agent reads to know what the answers can be.
The same hunk updated submit's roster for exactly this (line 585: "already_reported, claim_not_token or not_found"), so the omission here looks like a miss rather than a choice.
Suggested, on line 515: … status ok, empty, not_found, already_claimed or claim_not_token. <<GUIDE>> .
Non-blocking: nothing reaches the four new wiringsEvery one of the nine new facts calls the
Delete any one of those four arguments and all nine facts still pass green, because each fact supplies the value the dropped argument was meant to carry. That is the exact failure mode #6593 round 3 already filed against this feature, and the suite already has the shape that catches it — One fact in that shape would cover all four rows: build a host serving Not blocking on its own; raising it because the feature it guards is an identity boundary and the repo has already paid for this shape once. Things I checked that are fine
|
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — Before this change submit, the renewal and evidence were checked against the
claim_idonly — the diff's removed lines: submit read the holder only to name the journal actor, renew went straight toRenewAsync, evidence testedClaimHolderAsync(...) is not null. - VERIFIED — A legacy caller could claim under a named agent's name — pre-change
ClaimAsynchad only thetokenName is not nullarm (DarlingMcpInvestigationTools.cs:403); a legacyagent_idmetIsValidAgentIdalone (:420). - VERIFIED — Submit, renew and evidence now take
served?.Policy.AgentTokenName—DarlingMcpInvestigationTools.cs:384,:533,:627. - VERIFIED — The refusal journals under the caller, not the holder —
tokenNameis the actor at:439,:554,:680, andjournalAs = tokenName ?? agent(:676) carries it to the three later submit refusals (:688,:701,:719); pinned byANamedTokens_OtherSubmitRefusals_AreJournaledAsItself_NotAsUnclaimedandTheHoldersOwnRefusals_StillJournalAsTheHolder. - VERIFIED — Evidence checks the holder only while the request is claimed, as before — the guard is inside
if (request.Value.Row.State == "claimed")(:548). - VERIFIED — A legacy claim under a served named name is refused
agent_id_is_named_tokenunder theinvalidactor —:411-417,InvestigationRules.cs:40,InvestigationRequestStore.cs:1102; pinned byALegacyClaim_UnderAServedNamedTokensName_IsRefused_AndOtherNamesClaimasserting0|0|1. - VERIFIED — Legacy single-token mode behaves as before — no named tokens means
perRequestfalse (DarlingMcpHostService.cs:1263), nullAgentTokenName(DarlingMcpWritePolicy.cs:279) and an emptyNamedAgentNames(DarlingMcpThresholdOverrideTools.cs:36), so all three guards are inert; pinned byALegacyClaim_WithNoNamedTokensServed_AcceptsAnyValidName. - VERIFIED — Operator names are excluded and their actor is
operator:<name>, neveragent:—DarlingMcpTokenRoles.cs:61,DarlingMcpWritePolicy.cs:289,:279. Thetokens!atDarlingMcpHostService.cs:1337is guarded by itsperRequestbranch. - VERIFIED — Lite is untouched and owes no counterpart — only
Darling/paths in the diff, andAgentTokenName,McpTokenEntryandmcp.tokenshave no occurrence underLite/. - UNVERIFIED — "New class on the rig: Total 9, Failed 0" and the revert proof (5 of 9 fail with the checks disabled) — the nine
[Fact]s exist, but all self-skip onAssert.SkipWhenwithoutDARLING_TEST_PG(InvestigationClaimTokenLiveTests.cs:55); a live store and two builds were needed. - UNVERIFIED — The
*Investigation*/*McpTool*/ Stage=Guard sweeps at Total 1422 and Total 450, Failed 0 — needed a run. - UNVERIFIED — "Build: 0 warnings, 0 errors" — needed a Windows net10.0 build; the new types and usings do resolve on a read.
- REFUTED — "Submit, renew and evidence … refused with the new status
claim_not_token", and the CHANGELOG's "submitting a report, renewing a claim or reading evidence … is refused with claim_not_token" — renew returnsMcpHelpers.Refusal("claim_id", …)(:440), andRefusalisStatus("invalid", …)(McpHelpers.cs:946).claim_not_tokenreaches that caller only as the journalreason. The PR's own test asserts"invalid"(InvestigationClaimTokenLiveTests.cs:162), andclaim_investigation_request's roster (:362) lists neither status while submit's was updated in the same diff (:585). Commented inline.
Riskiest lines
Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs:677— the submit guard: get theholder is not nullconjunct wrong and either a named token reports on a stranger's claim, or a holder of a lapsed claim is refused instead of answeringlease_lost— covered byASecondNamedToken_CannotSubmitOnTheFirstTokensClaim_AndTheRefusalJournalsTheSecondandANamedTokens_OtherSubmitRefusals_AreJournaledAsItself_NotAsUnclaimed.Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs:436— the renew guard, now ahead ofRenewAsync: a droppedtokenName is not nullbreaks the legacy renewal — covered byASecondNamedToken_CannotRenewTheFirstTokensClaim_ButTheHolderCanandOnTheLegacyToken_ARenewalByClaimIdStillWorks.Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpHostService.cs:1337—tokens!.AgentNames, the only thing making the legacy guard live in production — UNCOVERED. Drop the argument and all nine facts still pass, because each suppliesnamedAgentNamesby hand. Same gap forDarlingMcpInvestigationTools.cs:385,:533,:627; the suite already has the catching shape atAgentTokenIdentityGateLiveTests.cs:232. Commented at top level.Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs:411— the legacy guard is anelse ifahead of the renewal block, so a legacy caller passingagent_idequal to a served named name is refused even when it only meant to renew a claim it already holds under that name. Claim path covered byALegacyClaim_UnderAServedNamedTokensName_IsRefused_AndOtherNamesClaim; the renew-under-that-name path UNCOVERED. Noted, not raised as a defect — the claim can only lapse.
Traps near this diff
- "Lite and Darling ship in parity (CONTRIBUTING's two-store rule): a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit — the census in
McpPageContractTestscompares the two SKUs' payload keys by tool name for exactly this (#3594)." — does not apply — named tokens are Darling-only:Lite/Mcp/McpInvestigationTools.cs:113and:252take notokenName. - "Ruling (#5833): tests are load-bearing. … never answer a trap with a test that reads source text or asserts how code is spelled (a signature at a line, a SQL fragment's wording, a comment's text, a trait roster, a coverage percentage)." — does not apply — all nine facts drive the store and assert answers and journal rows (
InvestigationClaimTokenLiveTests.cs:109,:219). - "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted (#3684). Alias, do not copy." — does not apply —
ClaimNotTokenandAgentIdIsNamedTokenare singleinternal constdeclarations (DarlingMcpInvestigationTools.cs:880,:886) that the messages and the tests both read.
Divergence (blind read vs PR body)
Two places, both on the renew arm.
-
The body and the CHANGELOG name
claim_not_tokenas the status for all three arms.:440→McpHelpers.cs:946says renew answersinvalid, withclaim_not_tokenonly in the journal. The author's own test agrees with the code, so the account is ahead of the code rather than the code being wrong — but the CHANGELOG is what ships, andclaim_investigation_request's description documents neither status, so an agent cannot learn this refusal from the surface it reads. -
The Tests section reads as end-to-end coverage. Every fact calls the
internalhelper with the identity passed as an argument, so nothing exercisesserved?.Policy.AgentTokenNameorserved?.NamedAgentNames— the four lines where a bearer token becomes an identity. A silence rather than a contradiction, but it is the difference between "the rule is tested" and "the rule is wired".
The rest matched. "Considered, not added" holds up: claim_id is projected by neither ListSql (InvestigationRequestStore.cs:408) nor ReportsRequestsSelect (:501) nor ReportsSql (:541), and SweepLapsedSql (:568) puts it in once_key, which ReportsJournalSql (:559) does not select.
Reviewer checklist
- Run the new class on the rig with
DARLING_TEST_PGset and confirm 9/9 — it self-skips without it, so a green CI leg proves nothing about these facts. - Re-run the revert proof after deciding the status question on line 440: if renew moves to
Status(ClaimNotToken, …),InvestigationClaimTokenLiveTests.cs:162changes with it. - On a real host with two agent tokens plus a legacy token in
mcp.tokens:tools/call submit_investigation_reportwith the second bearer and the first'sclaim_id, andclaim_investigation_requeston the legacy bearer withagent_id= the first token's name. Read the refusals and the journal actor from the store — the only thing that exercisesDarlingMcpHostService.cs:1337and the threeserved?.call sites. - Confirm the build is clean on Windows/net10.0.
…r claim; renew answers claim_not_token; reserved token names; host tests A legacy-token caller is refused (agent_id_is_named_token) when it renews, submits or reads evidence under a claim a served named token holds, and when an agent_action carries a served name as its agent_id. Store-level submit refusals journal as the caller. The names invalid and unclaimed are reserved for token names. The Darling-only statuses are listed after the GUIDE marker so the shared heads stay identical to Lite.
|
Follow-up review of The code is correct and well covered. I traced the new "a served named token's identity can't be hijacked" check through both gates:
One thing worth flagging: the PR body is now stale against this commit. Its "Considered, not added" section says, of the current PR:
Commit |
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — "Submit, renew and evidence take the calling token's name... refused with the new status
claim_not_token" —DarlingMcpInvestigationTools.ClaimTokenRefusal(new in this commit) is wired intoClaimAsync,EvidenceAsyncandSubmitAsync;InvestigationNamedTokenHostLiveTests.TheSecondToken_SubmittingOnTheFirstTokensClaim_IsRefusedAndJournaledAsTheSecondexercises it end to end. - VERIFIED — "Submit refusals now journal under
tokenName ?? holder ?? "unclaimed"" —DarlingMcpInvestigationTools.csSubmitAsync'sjournalAs = tokenName ?? agent(whereagent = holder ?? UnclaimedAgent), covered byAStoreLevelSubmitRefusal_IsJournaledAsTheCaller_NotAsUnclaimed. - VERIFIED (commit message, not PR body) — "A legacy-token caller is refused (agent_id_is_named_token) when it renews, submits or reads evidence under a claim a served named token holds, and when an agent_action carries a served name as its agent_id" —
ClaimTokenRefusal'stokenName is nullbranch, plus the matching new block in bothDarlingAgentActionGate.RunAsyncandServerActionGate.AuthorizeAsync; covered byInvestigationClaimTokenLiveTests.TheLegacyToken_CannotRenewSubmitOrReadEvidence_OnAServedNamedTokensClaim_...andInvestigationNamedTokenHostLiveTests.TheLegacyTokens_Level2Actions_AsAServedTokensName_AreRefused_OnBothGates. - UNVERIFIED — the PR body's cited rig run counts ("New class on the rig: Total 9, Failed 0...", "Total 1422, Failed 0...", "Build: 0 warnings, 0 errors") — these describe live test-rig/build output from before this commit; a read-only tree has no way to re-run them.
- REFUTED — PR body, "Considered, not added: Journaling those legacy pre-gate argument refusals under invalid... Not added" and "Refusing a legacy-token call that presents a named agent's claim_id... Not added" — this follow-up commit (
1ad885cd2) adds both:namedAgentNamesis threaded through everyServerActionGate.AuthorizeAsync/DarlingAgentActionGate.RunAsynccall site and journals the new refusal underInvestigationRules.InvalidAgentId(DarlingAgentActionGate.cs,ServerActionGate.cs), andClaimTokenRefusal'stokenName is nullbranch refuses a legacy call on a named token's held claim_id (DarlingMcpInvestigationTools.cs). The commit message correctly describes this; the top-level PR body was not updated to match.
Riskiest lines
Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs(newClaimTokenRefusal, ~line 895) — the single chokepoint deciding whether a claim/submit/evidence call may act under aclaim_idit doesn't own — covered byInvestigationNamedTokenHostLiveTests.TheSecondToken_SubmittingOnTheFirstTokensClaim_IsRefusedAndJournaledAsTheSecondandTheLegacyToken_UsingAServedTokensClaim_IsRefused_ForSubmitAndEvidence.Darling/PerformanceMonitor.Darling.Service/Mcp/ServerActionGate.cs(new block, ~line 88) — the legacy-token-vs-named-token-name collision check for Level 3 server actions (kill/force/evict/recompile/plan-guide) — covered byInvestigationNamedTokenHostLiveTests.TheLegacyTokens_Level2Actions_AsAServedTokensName_AreRefused_OnBothGates(itskill_sessionarm).Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs(SubmitAsync,journalAs = tokenName ?? agent) — changes who a store-level submit refusal (e.g.lease_lost) is journaled under — covered byAStoreLevelSubmitRefusal_IsJournaledAsTheCaller_NotAsUnclaimed.
Traps near this diff
- "Lite and Darling ship in parity... a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit" — does not apply: named agent tokens (#6542) are a Darling-only MCP feature (no
NamedAgentNames/AgentIdIsNamedTokenanywhere outsideDarling/), and the docstring restructuring in this commit actually restores byte-identical shared heads withLite/Mcp/McpInvestigationTools.cs(comparedclaim_investigation_requestandsubmit_investigation_report's text up to<<GUIDE>>) rather than breaking parity. - "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted" — does not apply: the near-identical named-token-collision blocks added to
DarlingAgentActionGate.RunAsyncandServerActionGate.AuthorizeAsyncare two gates with different outcome types (AgentActionGateOutcomevsServerActionDecision), following the pre-existing duplication pattern already in both files (e.g. theAgentIdNotTokencheck), not a new pinned-constant pair.
Divergence (blind read vs PR body)
The PR body's "Considered, not added" section describes two mitigations as deliberately deferred; this follow-up commit implements both of them (see the REFUTED claims above). The commit message is accurate about this; the PR body is not and should be updated before merge so it doesn't misdescribe the PR's final state.
Reviewer checklist
- Update the PR body's "Considered, not added" section (or add a new section) to reflect that commit
1ad885cd2implemented both previously-deferred mitigations, so the merged description matches the shipped behavior. - Re-run the live-Postgres
Darling.Testssuite (DARLING_TEST_PGset) to confirmInvestigationNamedTokenHostLiveTestsand the newInvestigationClaimTokenLiveTestsfacts pass on CI's rig — a read-only tree can't execute them.
review-pass: follow-up head=f484b4edbfc67089e5ccf470a75bd14d73b39c8e
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — "Submit, renew and evidence take the calling token's name... refused with the new status
claim_not_token" —DarlingMcpInvestigationTools.ClaimTokenRefusal(new in this commit) is wired intoClaimAsync,EvidenceAsyncandSubmitAsync;InvestigationNamedTokenHostLiveTests.TheSecondToken_SubmittingOnTheFirstTokensClaim_IsRefusedAndJournaledAsTheSecondexercises it end to end. - VERIFIED — "Submit refusals now journal under
tokenName ?? holder ?? "unclaimed"" —DarlingMcpInvestigationTools.csSubmitAsync'sjournalAs = tokenName ?? agent(whereagent = holder ?? UnclaimedAgent), covered byAStoreLevelSubmitRefusal_IsJournaledAsTheCaller_NotAsUnclaimed. - VERIFIED (commit message, not PR body) — "A legacy-token caller is refused (agent_id_is_named_token) when it renews, submits or reads evidence under a claim a served named token holds, and when an agent_action carries a served name as its agent_id" —
ClaimTokenRefusal'stokenName is nullbranch, plus the matching new block in bothDarlingAgentActionGate.RunAsyncandServerActionGate.AuthorizeAsync; covered byInvestigationClaimTokenLiveTests.TheLegacyToken_CannotRenewSubmitOrReadEvidence_OnAServedNamedTokensClaim_...andInvestigationNamedTokenHostLiveTests.TheLegacyTokens_Level2Actions_AsAServedTokensName_AreRefused_OnBothGates. - UNVERIFIED — the PR body's cited rig run counts ("New class on the rig: Total 9, Failed 0...", "Total 1422, Failed 0...", "Build: 0 warnings, 0 errors") — these describe live test-rig/build output from before this commit; a read-only tree has no way to re-run them.
- REFUTED — PR body, "Considered, not added: Journaling those legacy pre-gate argument refusals under invalid... Not added" and "Refusing a legacy-token call that presents a named agent's claim_id... Not added" — this follow-up commit (
1ad885cd2) adds both:namedAgentNamesis threaded through everyServerActionGate.AuthorizeAsync/DarlingAgentActionGate.RunAsynccall site and journals the new refusal underInvestigationRules.InvalidAgentId(DarlingAgentActionGate.cs,ServerActionGate.cs), andClaimTokenRefusal'stokenName is nullbranch refuses a legacy call on a named token's held claim_id (DarlingMcpInvestigationTools.cs). The commit message correctly describes this; the top-level PR body was not updated to match.
Riskiest lines
Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs(newClaimTokenRefusal, ~line 895) — the single chokepoint deciding whether a claim/submit/evidence call may act under aclaim_idit doesn't own — covered byInvestigationNamedTokenHostLiveTests.TheSecondToken_SubmittingOnTheFirstTokensClaim_IsRefusedAndJournaledAsTheSecondandTheLegacyToken_UsingAServedTokensClaim_IsRefused_ForSubmitAndEvidence.Darling/PerformanceMonitor.Darling.Service/Mcp/ServerActionGate.cs(new block, ~line 88) — the legacy-token-vs-named-token-name collision check for Level 3 server actions (kill/force/evict/recompile/plan-guide) — covered byInvestigationNamedTokenHostLiveTests.TheLegacyTokens_Level2Actions_AsAServedTokensName_AreRefused_OnBothGates(itskill_sessionarm).Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInvestigationTools.cs(SubmitAsync,journalAs = tokenName ?? agent) — changes who a store-level submit refusal (e.g.lease_lost) is journaled under — covered byAStoreLevelSubmitRefusal_IsJournaledAsTheCaller_NotAsUnclaimed.
Traps near this diff
- "Lite and Darling ship in parity... a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit" — does not apply: named agent tokens (#6542) are a Darling-only MCP feature (no
NamedAgentNames/AgentIdIsNamedTokenanywhere outsideDarling/), and the docstring restructuring in this commit actually restores byte-identical shared heads withLite/Mcp/McpInvestigationTools.cs(comparedclaim_investigation_requestandsubmit_investigation_report's text up to<<GUIDE>>) rather than breaking parity. - "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted" — does not apply: the near-identical named-token-collision blocks added to
DarlingAgentActionGate.RunAsyncandServerActionGate.AuthorizeAsyncare two gates with different outcome types (AgentActionGateOutcomevsServerActionDecision), following the pre-existing duplication pattern already in both files (e.g. theAgentIdNotTokencheck), not a new pinned-constant pair.
Divergence (blind read vs PR body)
The PR body's "Considered, not added" section describes two mitigations as deliberately deferred; this follow-up commit implements both of them (see the REFUTED claims above). The commit message is accurate about this; the PR body is not and should be updated before merge so it doesn't misdescribe the PR's final state.
Reviewer checklist
- Update the PR body's "Considered, not added" section (or add a new section) to reflect that commit
1ad885cd2implemented both previously-deferred mitigations, so the merged description matches the shipped behavior. - Re-run the live-Postgres
Darling.Testssuite (DARLING_TEST_PGset) to confirmInvestigationNamedTokenHostLiveTestsand the newInvestigationClaimTokenLiveTestsfacts pass on CI's rig — a read-only tree can't execute them.
Claude posting for Erik Darling. Superseded: the round-1 items were fixed in f484b4e.
Claude posting for Erik Darling.
Review fixes
Round 1 fixes are folded into the sections below: the legacy token can no longer renew, submit, read evidence or act (Level 2 and server actions) under a served named token's claim or name; store-level submit refusals journal as the caller;
invalidandunclaimedare reserved token names; renew answersclaim_not_tokenlike submit and evidence; the status rosters in the three tool descriptions are updated, with the Darling-only statuses after the<<GUIDE>>marker (fixes the two contract tests CI failed on); tool-entry and real-host tests cover the served policy and names wiring.What was wrong
With named agent tokens served, a claim's later calls were checked against the
claim_idonly, not against the token that made the claim:submit_investigation_report, the claim renewal (claim_investigation_requestwith aclaim_id) andget_investigation_evidenceaccepted any named token that presented another token'sclaim_id. Refusals on submit were journaled under the claim's holder (or "unclaimed"), not the caller.agent_idequal to a named agent's name and take the claim, so the journal credited that named agent with the claim and with plan changes made under it.agent_action.agent_idset to that name.What changed
A claim's later calls must come from the token that claimed it, and the legacy token may not act under a served named agent's name or claim.
Mcp/DarlingMcpInvestigationTools.csclaim_not_tokenand journaled under the caller's name.agent_id_is_named_tokenwhen it claims with anagent_idthat is a served name, and when it renews, submits or reads evidence under a claim whose holder is a served name. These rows journal asagent:invalid, never as the named agent.journalAs), including the store-level ones (lease_lost,already_reported, closed); they used to journal asagent:unclaimed.InvestigationToolCore.Status(<status>, message), so the renew arm answersclaim_not_tokenlike submit and evidence (it answeredinvalid).<<GUIDE>>marker ofclaim_investigation_request(claim_not_token,agent_id_is_named_token),get_investigation_evidenceandsubmit_investigation_report(same two), so the heads shared with Lite stay byte-identical and keep the shared status contract (this was the CI failure on the previous head).DarlingAgentActionGate.csandServerActionGate.cs: a legacy-tokenagent_actionwhoseagent_idis a served name is refused withagent_id_is_named_tokenand journaled asagent:invalid. The names reach the gates throughDarlingAgentActionSeat,DarlingMcpServedPolicy(mute, custom rule, signature merge and threshold override tools) and a newnamedAgentNamesargument on the plan force, evict, kill, plan guide and recompile tools.DarlingMcpTokenRoles.cs:IsValidNamereservesinvalidandunclaimed(any case), the two placeholder actors refusal rows use. The config check names the reserved words and does not serve that token.DarlingMcpHostService.cs: the host passes the served agent names (tokens.AgentNames) to the policy and the seat. Operator-role token names are not in the set (their journal actor isoperator:<name>).Named tokens have not shipped: the commit that added them (#6556, 2026-10-22 23:35) is after the
v3.11.0tag (11:26 the same day) andgit tag --containslists no release. So no released config can carry a token namedinvalidorunclaimed, and the reserved names need no migration note beyond the CHANGELOG line.A legacy caller that passes an
agent_idequal to a served name is refused even when it means to renew a claim it holds under that name. After this change that renew is refused anyway (the claim's holder is a served name), so the two rules agree. The named token takes the claim over by claiming again under its own name (statusrenewed,reclaimed: true, same claim_id).Tests
InvestigationClaimTokenLiveTests(live PostgreSQL, now 12 tests): the renew test assertsclaim_not_token; the legacy-claim test assertsagent_id_is_named_token; new tests for the legacy token refused on renew, submit and evidence under a named token's claim (rows journal asagent:invalid), the named token reclaiming under its own name and then submitting, and alease_lostrefusal journaled as the calling token and notagent:unclaimed.InvestigationNamedTokenHostLiveTests(live PostgreSQL, 5 tests) through the real host (BuildServerwith two agent tokens plus the legacy token,ToolsCallAsync): the second token'ssubmit_investigation_reportandget_investigation_evidenceon the first token's claim answerclaim_not_tokenwith the row naming the second token; the legacy bearer'sclaim_investigation_requestunder a served name, and itssubmit_investigation_reportandget_investigation_evidenceunder a named token's claim, answeragent_id_is_named_token; itscreate_mute_rule(the seat) andkill_session(the server action gate) as a served name are refused.BuildServergained an optionalownerargument so the server gate can run.DarlingMcpNamedTokenTests: two new bad-entry cases (invalid,Unclaimed) for the reserved names.tokens!.AgentNamesat the policy registration fails 3 host tests; droppingserved?.NamedAgentNamesat the claim, the evidence and the submit tool entries each fails one host test; dropping the served policy from the evidence tool entry fails the second-token evidence test; dropping the seat's names or the kill tool's names fails the Level 2 host test; returningagentinstead ofjournalAsto the store fails thelease_losttest; disabling the legacy-holder check fails the legacy renew/submit/evidence test.*Investigation*,*Claim*,*AgentAction*,*ServerActionGate*,*TokenRoles*,*NamedToken*,*AgentToken*,*DarlingMcpTokenSet*(before the last merge of origin/dev): Total 670, Failed 0, Skipped 0. Earlier on the same build:InvestigationToolContractTestsandMcpToolGuideTests(the two classes CI failed) pass. The Census, Ratchet,*Contract*,*ToolGuide*and*McpPage*classes were run on the merged head (after the final merge of origin/dev), together with the two live classes above: Total 759, Failed 0, Skipped 0.Considered, not added
claim_not_tokentells a caller that a GUID it already holds is another token's claim on this request. claim_ids are never listed, so the caller learns nothing it could not already use.agent_idit passed (agentTokenName ?? action.AgentId). That row is a refusal only (no claim or change is credited), so it was left; threading the names there is a larger change.CHANGELOG entry
A claim's later calls must now come from the token that claimed it. With named agent tokens, submitting a report, renewing a claim or reading evidence with another token's claim_id is refused with
claim_not_token(renew included) and journaled under the calling token. The legacy token, when named tokens are served, is refused withagent_id_is_named_tokenwhen it claims under a named token's name, acts under a claim held by a named token, or passes a named token's name as theagent_idof anagent_action, so the journal no longer credits a named agent with what the legacy token did. A submit refused by the store (lease_lost,already_reported, closed) now journals as the calling token and notagent:unclaimed. The namesinvalidandunclaimedare reserved: amcp.tokensentry with either name (any case) is not served and the config check names the reserved words (named tokens are not in a release yet, so no released config is affected).