Skip to content

Darling MCP: an operator token sets route agent fields, part 2 of #6542 - #6593

Merged
erikdarlingdata merged 11 commits into
devfrom
fix/6542-route-agent-fields
Oct 23, 2026
Merged

erikdarlingdata merged 11 commits into
devfrom
fix/6542-route-agent-fields

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Claude posting for Erik Darling.

Part 2 of #6542. Closes #6542. Closes #6395. (The base is dev, so close both issues by hand at merge.)

What was wrong

A route's agent fields (agent level, the four limits, the fallback of an agent route) could be set only in the Viewer or on the web dashboard. An operator agent that needed to set them had no MCP path. On one store it wrote the route rows with store-owner SQL instead. That path skipped NotificationRouteWriter.Validate and wrote no route_agent_changed journal row.

What changed

All paths are under Darling/.

  • create_notification_route and update_notification_route take agent_level (0 to 3), agent_max_per_hour, agent_max_per_day, agent_lease_minutes and agent_expiry_minutes. They take a DarlingMcpServedPolicy service parameter, so the client cannot supply who it is. Only an operator-class token may use the agent fields: a named operator token, or the legacy single token served mcp.allowWrites (Effective.IsOperator and Effective.OperatorActor, new in DarlingMcpWritePolicy.cs). The fields are checked with NotificationRouteWriter.Validate over the route as it would be stored. fallback_route_id was already a parameter. On an agent route, or on a route the same call gives agent fields, an operator token's fallback is written together with the agent fields.
  • New store function config.set_notification_route_agent (SECURITY DEFINER, pinned search_path, no dynamic SQL, EXECUTE to mcp only). It is in DarlingManagedRoles.BuildNotificationRouteFunctionsSql and in tools/provision-roles.sql, with the same text. It reads the row FOR UPDATE, checks the level, the bounds and the fallback again, writes the five agent columns and the fallback, and writes the route_agent_changed journal row in the same transaction. The row's actor is operator:<token name> for a named operator token, and operator: plus the token identity the route write log lines already carry (the fingerprint, token 1a2b3c4d) for the legacy token. It is never a token value. The function refuses any actor that is not operator: plus a name of that shape. The journal detail has the same before and after shape as the Viewer's save. A new route is journaled only when it sets a level, and an edit only when a level or a limit changed, as NotificationRouteWriter.AgentChange does.
  • Review round 1 (M2): an operator-class token changes every field of an agent route, not only the agent fields. config.update_notification_route and config.set_notification_route_destinations take the actor the service passes and refuse a route with an agent setting for any actor that is not operator:. The old signatures are dropped. set_notification_route_enabled and set_notification_route_fallback hand an operator token's change to such a route to the same path. delete_notification_route still refuses an agent route for every caller.
  • A legacy token served mcp.allowInvestigations with mcp.allowWrites off keeps today's behaviour: it is served no route write tool. An agent token still never sees or calls these tools (part 1, unchanged).
  • Item 7, the refusal journal identity: see below.
  • Item 10c: McpTokenEntry.ResolveToken now names mcp.tokens[<name>].token (or .encryptedToken) in its error, not mcp.network.token.

The level-3 decision

Erik, 10-23: MCP is full admin, so an operator token sets 0 to 3.

  • A named operator token sets agent levels 0 to 3 and changes a route at any level, level 3 included. Each change is journaled as route_agent_changed under operator:<name>.
  • An agent token still never reaches the route tools (part 1's tool set, and its test AnAgentToken_ListsNoRouteAdminTool_AndACallToOneIsRefusedAsUnknown, unchanged).
  • The legacy single token with mcp.allowWrites is operator-class and sets the agent fields too (see the role table).
  • A level 3 server action (force, unforce, evict, kill) also needs that server's switch in config.config_server_actions and a remediation login, whatever the route says. ServerActionGate.cs checks both. The switches are NOT person-only: set_server_action (DarlingMcpServerActionTools.cs) writes them for an operator-class token when mcp.allowWrites and mcp.allowServerAdmin are both on (allowServerAdmin is off by default). So with both on, an operator token can set a route to level 3 and turn a server's switch on. No agent token reaches either tool (both are in NeverAgentTools).
  • The remaining person-only barrier is the remediation login. The gate needs it as an env: or file: reference. Checked: edit_server refuses every remediation_* key (DarlingMcpServerAdminTools.Edit.cs), no other MCP tool or web route writes those columns, and the store trigger on config_monitored_servers refuses a reference (PW001) from every role but the owner, so only the configuration file sets one.

Roles (agreed with the coordinator, 10-23)

Token Gets
named operator full admin, never claim or submit
legacy with allowWrites the same as operator
named agent reads, investigations, and Level 2 through the gate, never NeverAgentTools
legacy with allowInvestigations the same as agent

Under R2 of #5632 the legacy token never holds both arms, so no investigation agent holds a token that can set route agent fields.

Overlap

The rung: none

No migration rung, so the schema stays at 214. The reasons:

  • The new function and its grant are built by the provisioning batch, which the owner runs on every start. They are not versioned schema, like create_notification_route and update_notification_route before them (Darling MCP: notification routes can't be created or edited, alert history can't be dismissed, and there is no quiet window #6231). A self-managed store gets them by re-running tools/provision-roles.sql, as it already does for those two functions.
  • No table or column changed. The ck_config_notification_routes_agent CHECK already allows levels 0 to 3 and the limit bounds.
  • Review round 1: update_notification_route and set_notification_route_destinations take a new p_actor argument, and set_notification_route_agent takes p_before. Each old signature is dropped with DROP FUNCTION IF EXISTS in the same batch and in tools/provision-roles.sql, so none stays callable. The only callers are the MCP route tools (DarlingMcpAlertTools.RouteAdmin.cs, through NotificationRouteWriter.SetRouteDestinationsAsync for destinations). The Viewer and the web write routes with NotificationRouteWriter.SaveAsync and plain table writes, and call none of these functions, so they are unchanged.
  • Self-provisioned stores: until a person re-runs tools/provision-roles.sql, the MCP route writes fail with undefined-function (42883) and roll back. They fail closed. The CHANGELOG entry tells them to re-run it.
  • The journal trigger (investigation_journal_owner_rows) refuses only the request event kinds and once keys other than its list. route_agent_changed is not a request event, and the function writes no once key. The trigger skips the owner, and the function runs as the owner. The mcp role's column INSERT grant on the journal is not used, because the definer writes the row. The live test runs with the trigger installed and the grants replayed from the batch.

Items 7 to 10 of #6556's security review

  • 7. Refusal rows under another agent's name. Fixed. Trigger: an agent token sends agent_action with another agent's agent_id, plus an argument the tool refuses early. It is easy to trigger, but it writes only audit noise, because the action is still refused. The fix is one agentTokenName ?? agentId at each journal site (RefuseEarlyAsync, the PostgreSQL target refusal in force and unforce, RefuseBeforeGateAsync with its seven callers, and LooseAgentId in the gate). Test: AgentTokenIdentityGateLiveTests.APostgresTargetRefusal_* and EveryRefusalBeforeTheGate_* assert one row as agent:triage-bot and none as agent:someone-else.
  • 8. Gate identity tests. Added. AgentTokenIdentityGateLiveTests.TheLevel2Gate_* (DarlingAgentActionGate) and TheServerActionGate_*, a theory over force, unforce, evict and kill. Each asserts the agent_id_not_token refusal, one refused row under the token's name, and no action_called row. No SQL Server is needed, because every refusal comes before a remediation login opens.
  • 9. Fail-closed start test. Added. DarlingMcpHostGateLiveTests.NamedTokensThatCannotBeServed_WithNoLegacyToken_StopTheStart_AndNothingAnswers drives the real TryStartServerAsync with two named tokens under the length floor and no legacy token. It asserts that the start returns false, that nothing answers on the port, that the Critical line was logged, and that no token value was logged.
  • 10. The Lows:
    • Unreadable named token names mcp.network.token. Fixed, with the test DarlingMcpNamedTokenTests.AnUnreadableNamedToken_NamesItsOwnSetting_NotTheLegacyOne.
    • Renew and submit bind to claim_id only. Considered, not added. Trigger: agent token B must present the claim_id of agent A's live claim. A claim_id is a random GUID that is returned only to the claimer, and no read tool lists it for another agent. Likelihood: very low, because it needs a leaked claim id. The fix is a name check in two more tool paths and their tests.
    • A legacy token beside named tokens may claim as any agent_id. Considered, not added. That is a supported setup the operator chooses, and the start already logs a Critical line for it. Refusing it would change the legacy token's behaviour, which the brief keeps exact.
    • The legacy parity test compares two branch builds. Considered, not added. Comparing against a dev build needs a second build in CI. The existing test pins the set to Effective.Offers for every allowWrites and allowInvestigations combination. This PR does not change the legacy policy, and the legacy route test below shows that.

Tests, with revert proofs

  • McpRouteAgentFieldsLiveTests (live PostgreSQL, runs as the mcp role with the batch's own grants):
    • AnOperatorToken_CreatesARouteAtLevel1_AndTheJournalNamesTheToken
    • AnOperatorToken_SetsLevel3_AndChangesARouteAtLevel3_EachJournaled
    • TheLegacyWritesToken_SetsLevels1And3_AndEachIsJournaled_ButAnInvestigationsTokenSetsNone, which includes the legacy token's send_resolutions: "always" step on a level 3 route.
    • New: AnOperatorToken_ChangesSendResolutionsAndEnabled_OnALevel3Route_AndTheStoreRefusesAnyOtherActor
    • New: OneCall_ChangingTheMatchAndTheLevel_JournalsBoth_WithTheOldMatchInBefore
  • AgentTokenIdentityGateLiveTests (live PostgreSQL): items 7 and 8, plus new EachKillRefusalBeforeTheGate_IsJournaledUnderTheTokensName (theory, 5 cases) and AKillCallThroughTheHost_WithANamedAgentToken_JournalsTheTokensName.
  • One fact in each of DarlingMcpHostGateLiveTests and DarlingMcpNamedTokenTests: items 9 and 10c.
  • McpRouteRoleLiveTests, McpRouteDestinationsLiveTests and McpRouteDestinationAnswersLiveTests call the functions directly with a NULL actor and keep their refusals, so the non-operator path keeps its pins.

Revert proofs. They ran in CI after the merge, on four throwaway PRs that were then closed (results below). Each guarded change and the test that must fail without it:

Guarded change Revert Test that must fail
set_notification_route_agent call in CreateNotificationRouteAsync remove the call AnOperatorToken_CreatesARouteAtLevel1_* (stored level, journal count)
No level 3 refusal in AgentFieldRefusal put one back AnOperatorToken_SetsLevel3_* (created)
OperatorActor for the legacy token return TokenName alone TheLegacyWritesToken_* (created)
agentTokenName in RefuseBeforeGateAsync restore parsed.AgentId EveryRefusalBeforeTheGate_* (0|1)
return false after the named-token Critical line delete it NamedTokensThatCannotBeServed_* (started)
M2: NOT v_operator on the agent-route DL403 in update_notification_route drop the AND NOT v_operator AnOperatorToken_ChangesSendResolutionsAndEnabled_* and TheLegacyWritesToken_* (refused, not updated)
M2: the actor check is an actor check, not gone delete the DL403 block AnOperatorToken_ChangesSendResolutionsAndEnabled_* (no DL403 for NULL and agent:) and McpRouteRoleLiveTests agent-route refusal
M2: actor passed to update_notification_route pass NULL in UpdateNotificationRouteAsync AnOperatorToken_ChangesSendResolutionsAndEnabled_* (refused)
L2: p_before from the update function pass NULL in SetRouteAgentAsync OneCall_ChangingTheMatchAndTheLevel_* (reason and before match)
array_append in the agent function restore v_changed || 'agent_level' every journal assertion in McpRouteAgentFieldsLiveTests (malformed array literal)
L3: agentTokenName at the kill bad-session site drop the argument EachKillRefusalBeforeTheGate_*(bad_session) and AKillCallThroughTheHost_*
L3: the same at bad-login-time, Azure database, PG bad backend id, PG pid mismatch drop the argument at that site the matching EachKillRefusalBeforeTheGate_* case
L3: served?.Policy.AgentTokenName in the KillSession wrapper pass null AKillCallThroughTheHost_* (0|1)
L3: agentTokenName passed to RunKillPostgresAsync drop it EachKillRefusalBeforeTheGate_*(pg_bad_backend_id) and (pg_pid_mismatch)

Revert proof results. Four throwaway PRs ran in CI, on this PR's final head with some changes reverted. Each PR was closed without merge.

Round Reverted Result
1 agentTokenName in RefuseBeforeGateAsync, the return false, AND NOT v_operator, p_before, and the five kill sites Failed as the table says: EveryRefusalBeforeTheGate_*, NamedTokensThatCannotBeServed_*, AnOperatorToken_ChangesSendResolutionsAndEnabled_*, TheLegacyWritesToken_*, OneCall_ChangingTheMatchAndTheLevel_*, AKillCallThroughTheHost_* and all five EachKillRefusalBeforeTheGate_* cases.
2 the NULL actor in UpdateNotificationRouteAsync, the KillSession wrapper, and RunKillPostgresAsync's argument Failed as the table says: AnOperatorToken_ChangesSendResolutionsAndEnabled_*, AKillCallThroughTheHost_*, and EachKillRefusalBeforeTheGate_*(pg_bad_backend_id) and (pg_pid_mismatch).
3 the DL403 block, and the legacy OperatorActor AnOperatorToken_ChangesSendResolutionsAndEnabled_* and TheLegacyWritesToken_* failed. McpRouteRoleLiveTests did not fail. Its agent-route refusal comes from the MCP layer before the store, so that row's second test does not depend on the DL403 block. The first test proves the block.
4 the create-path set_notification_route_agent call, and a level 3 refusal put back Failed as the table says: AnOperatorToken_CreatesARouteAtLevel1_* and AnOperatorToken_SetsLevel3_*.

The array_append row was proven earlier.

Every failure above is an assertion failure. Rounds 1, 3 and 4 also failed the bring-your-own-script grant tests. That came from dev, not from the reverts: this PR and #6613 merged without a CI run on both. #6644 fixes it.

What ran where:

  • macOS: Darling.Tests builds with 0 errors (-p:EnableWindowsTargeting=true). Darling.Tests targets net10.0-windows and cannot run on macOS.
  • A throwaway PostgreSQL 16 container (removed afterwards) ran the three functions from tools/provision-roles.sql over a minimal table: the old signatures are gone, a match plus level call journals "changed metric_match, agent_level" with the old match in before, an operator actor changes send_resolutions and enabled on a level 3 route, NULL and agent: actors get DL403, and a non-operator agent actor gets DL400. That run also found that the head commit's v_changed || 'agent_level' fails with "malformed array literal", so every journaled agent change rolled back. Fixed with array_append.
  • CI's Windows build and Darling PostgreSQL tests are the only full runs of these tests. See the CI state below.

Review round 1

The Opus security review found no path for an agent-class token. Its findings and the fixes:

  • M2, and Darling MCP: admin tool for notification route agent settings (agent level, Off) #6395's "every route tool refuses an agent route". Fixed. See What changed. The C# gate (the per-request tool filter, NeverAgentTools and IsOperator) is the real gate. In the store the DL403 guard is now an actor check, the same shape set_notification_route_agent uses.
  • M1, a wrong premise. Fixed in the text. The level-3 paragraph now says set_server_action writes the switches for an operator token when mcp.allowWrites and mcp.allowServerAdmin are both on. The "Next:" line is gone, and the comment in ServerActionGate.cs is corrected. The remediation login is the remaining person-only barrier, and that claim was checked (see The level-3 decision).
  • L1, the SQL actor check is a label. Every token connects as the same mcp role, so any mcp session can pass operator:anyone. The SQL actor check is a label, and the C# gate is the real one. The test comment now says so. No code: no MCP path to raw SQL was found. It does mean the database no longer stops the mcp role from making an agent route, so a future SQL-execution bug on the MCP surface would reach level 3.
  • L2, the journal missed a match or scope change in the same call. Fixed. update_notification_route returns the row before the change, and the agent function journals it as before and names metric_match, scope_server_id, scope_tag_id and enabled as the Viewer does. An operator's match, scope or enabled change to a route with an agent setting is journaled even with no agent field. Test: OneCall_ChangingTheMatchAndTheLevel_*.
  • L3, untested call sites. Fixed. A theory over the five kill and PostgreSQL-kill refusals made before the gate, and one tools/call through the host.
  • L4, no upgrade trigger for self-provisioned stores. Fixed with a CHANGELOG sentence.
  • L5, no MCP path back to Inherit, and undo does not restore the agent fields. Follow-up, not this round. Undo belongs to the Darling MCP: mute-rule and route toggles return the previous value and an undo call (part of #6403) #6514 rebase.
  • L6, no test had run. CI runs on this push.
  • Found in this round: the agent function's v_changed || 'agent_level' failed at runtime. Fixed (see What ran where).

Considered, not added:

  • The reviewer's Speculative items: OperatorActor reading operator:token unknown if TokenIdentityOf throws (no trigger named), and p_created or p_before passed by raw SQL as mcp to fake or hide a journal row (needs raw SQL as mcp, which no MCP path gives).

Checked after the review, by a code read at this head (no test added, because each is a fact about the current code, not a change):

  • Item 10a: no read tool shows another agent's claim_id. Only claim_investigation_request returns one. It goes to the caller that claims, or back to the same named token on a reclaim. A claim by the agent holding the live lease returns its claim_id (#6576) #6586 allows a reclaim only on a named token. The lease_expired journal row keeps the claim id only in once_key, and no read selects that column.
  • The LooseAgentId case: ParseAgentAction refuses that JSON at the evidence check ("missing"). The request id, claim id, agent id and reason in it all parse. So the case reaches the gate's parse-refusal branch, which journals policy.AgentTokenName before it falls back to the loose agent id.

Handoff

  • Done: review round 1 is committed and pushed. The build is clean on macOS, and scrubcheck reports 0 hits on the diff, the commits and this body.
  • Left: the revert proofs in the table above, run by the coordinator in a separate CI round. L5 and the Darling MCP: mute-rule and route toggles return the previous value and an undo call (part of #6403) #6514 rebase.
  • CI: dev compiles again since A request's brief names the earlier report with the same root fact under another story hash (#6565) #6582. 8ad056afb merges dev into this branch. Its CI round failed 1 test of 5,171, and the fault was in the test: AnOperatorToken_ChangesSendResolutionsAndEnabled_* read a boolean column back as "True" and compared it with "true". f022441fc reads both boolean columns as text, as the test's other reads do. CI runs on that head.
  • CI at the old head 5a20f5b failed 4 tests. This head fixes each one: the 3 McpRouteAgentFieldsLiveTests facts answered error on create (the agent function's malformed array literal, now array_append), and EveryConvertedHead_StaysAtOrUnder620Characters read update_notification_route: 679. That head is 584 characters of head text here (648 at 5a20f5b), plus the 31-character guide pointer, so 615. create_notification_route is 545 plus 31. Lite has no route admin tools, so no Lite head shares these names.
  • Next step: when CI is green, run the revert proofs in the table above in two CI rounds, each test the target of one revert per round. Record each result in the table.

CHANGELOG entry

Darling MCP: an operator token (a named operator token, or the single token with mcp.allowWrites) can set a notification route's agent level (0 to 3), its limits and its fallback through create_notification_route and update_notification_route. The Viewer's rules check each change, and a route_agent_changed journal row names the token. An operator token also changes the match, scope, enabled flag, resolution override, fallback and destinations of an agent route. Refusal rows from an agent token are journaled under the token's name (#6542). Re-run tools/provision-roles.sql after upgrading if you provision the store's roles yourself.

…#6542 part 2)

create_notification_route and update_notification_route take agent_level, the four limits and
fallback_route_id for a named operator token only, checked with NotificationRouteWriter.Validate and
written by config.set_notification_route_agent, which journals route_agent_changed as operator:<name>.
Level 3 is refused over MCP, and a route at level 3 is not changed over MCP. Early agent refusals
journal under the agent token's name. A named token's resolve error names its own setting.
…6542 part 2)

MCP is full admin: the level 3 refusals are removed from the tool and the store function, and the
test now asserts level 3 is set, changed and lowered by an operator token, each change journaled.
…ields (#6542 part 2)

Agreed with the coordinator: under R2 of #5632 that token never holds the investigation tools, so it
sets agent levels 0 to 3 and edits agent routes like a named operator token. Its journal actor is
operator: plus the token identity the route write logs already carry. A token served the
investigations sets no agent field.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 23, 2026 05:03

if (AgentRouteRefusal(existing, "update_notification_route") is { } agentRefusal)
agent ??= RouteAgentRequest.None;
if (AgentRouteRefusal(existing, "update_notification_route", operatorActor) is { } agentRefusal)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An operator token's update of an agent route is still refused by the store function — including the one this PR's own test asserts succeeds.

Passing operatorActor here makes AgentRouteRefusal return null for every operator-class caller, so the call proceeds. But config.update_notification_route is unchanged and refuses the whole row, not just match and scope:

IF v_row.agent_level IS NOT NULL AND v_row.agent_level >= 1 THEN
   RAISE EXCEPTION 'That route is an agent route: it is changed in the Viewer, not over MCP.' USING ERRCODE = 'DL403';

(DarlingManagedRoles.cs:2703-2705, tools/provision-roles.sql:1477-1479)

So whenever columns.Count > 0 on a route at agent_level >= 1, the transaction aborts at line 374 before SetRouteAgentAsync ever runs, RouteFunctionOutcome maps DL403 to refused, and nothing is written — not the non-agent column, not the agent fields.

Failing scenario, which is exactly McpRouteAgentFieldsLiveTests.cs:151-153:

  1. operator token creates "Blocking" with agent_level: 1 → created
  2. operator token sets agent_level: 3 → updated (columns empty, so only the agent function runs)
  3. operator token sends send_resolutions: "always" → columns = ['send_resolutions'], route is at level 3 → DL403 → {"status":"refused","…agent route: it is changed in the Viewer, not over MCP"}

The test asserts "updated". It will fail in Darling PostgreSQL tests (the PR body says no test has run yet). The same happens for enabled: true, and for any call that mixes an agent field with enabled/send_resolutions/metric_match/scope on a route already at level ≥ 1 — the mixed call loses the agent write too.

Either relax the store function for the columns part 2 means to open (it holds the row's lock, so it is the right place to decide), or keep the C# guard for the non-agent columns and refuse the mixed call in words before the transaction opens. As written, the documented capability ("Only an operator token sets the agent fields") silently fails the moment a second field rides along.

/// including agent_level 0: that is the route's explicit Off (null inherits, 0 is off, 1 investigates), so it opens
/// no investigation. It can be turned on over MCP; turning it off or deleting it is <see cref="OffRouteRefusal"/>'s (#6281).
/// <para>#6542 part 2: an operator-class caller (<paramref name="operatorActor"/>: a named operator token, or the legacy token with writes) passes, so update_notification_route can set the agent
/// fields of an agent route. The store functions still refuse its match, scope and destinations, and level 3 stays person-only.</para>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two statements in this new paragraph are contradicted by the code it documents:

  • "level 3 stays person-only" — config.set_notification_route_agent accepts 0, 1, 2, 3 (DarlingManagedRoles.cs:2808), AgentFieldRefusal adds no level ceiling, and the PR body's own "level-3 decision" section says an operator token sets 0 to 3. The sentence that is true is the one in the function's header comment: a level 3 server action still needs config.config_server_actions. Worth saying that instead, since this comment sits on the gate a reader checks first.
  • "The store functions still refuse its match, scope and destinations" — they refuse considerably more than that: config.update_notification_route refuses every column on a route at agent_level >= 1. That is the defect noted on DarlingMcpAlertTools.RouteAdmin.cs:245.

{
internal static readonly RouteAgentRequest None = new(null, null, null, null, null);

internal int? Fallback { get; init; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: Fallback is written (line 334) but never read — SetRouteAgentAsync takes the fallback as its own fallback parameter (line 500) and binds that at line 513, so this property is dead. Two carriers for one value, one of them unused, is the kind of pair that drifts later; either drop the property and keep the parameter, or drop the parameter and bind agent.Fallback.

s.StartsWith("GRANT EXECUTE ON FUNCTION config.create_notification_route", StringComparison.Ordinal)
|| s.StartsWith("GRANT EXECUTE ON FUNCTION config.update_notification_route", StringComparison.Ordinal)
|| s.StartsWith("GRANT EXECUTE ON FUNCTION config.set_notification_route_agent", StringComparison.Ordinal)).ToList();
Assert.Equal(3, executes.Count);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but this is the shape REVIEW-TRAPS.md rules against: "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, …)". executes is a prefix match over ViewerGrantReplay.BatchStatements(), so pinning it at 3 asserts how the provisioning batch is spelled, not what the role can do. A fourth GRANT EXECUTE ON FUNCTION config.update_notification_route… added for an unrelated reason reds this test with a message about a count.

The setup needs the three grants replayed; it does not need the count. Dropping the assert (or asserting each of the three prefixes was found, which is what the loop actually depends on) keeps the fixture honest without the pin.

@claude

claude Bot commented Oct 23, 2026

Copy link
Copy Markdown

Two notes that are not on a line this diff touches.

Darling/README.md now describes the opposite behaviour

Darling/README.md:867 is the operator-facing account of this surface (CONTRIBUTING names it as such) and it still says, in the same paragraph:

A route with an explicit agent setting (Off included) keeps its match and scope in the Viewer, and an agent route is changed only in the Viewer.

… the route tools write through config.create_notification_route, config.update_notification_route and config.set_notification_route_destinations, functions that store only a sealed value or an empty one for a webhook and refuse an agent route.

After this PR an operator token sets an agent route's level (0 to 3), its four limits and its fallback over MCP through a fourth function the paragraph does not name. An operator reading that paragraph would conclude the agent channel is unreachable from MCP, which is the decision this PR reverses. The PR body does not mention the README, so this reads as an omission rather than a deliberate deferral — worth one sentence in that paragraph plus config.set_notification_route_agent in the function list.

A smaller inconsistency the body does not raise

set_notification_route_fallback (DarlingMcpAlertTools.cs:844) still calls AgentRouteRefusal with no operatorActor, so an operator token is refused there while update_notification_route(route_id, fallback_route_id: …) on the same agent route now goes through the agent function (RouteAdmin.cs:326-335). Two doors to one column, one open and one shut. Not a correctness bug — just a seam an operator will trip over, and a candidate for the same treatment or an explicit note.

Related: because RouteAgentRequest.Columns only names a field whose value HasValue, agent_level can never be set back to NULL (inherit) over MCP — 0 (Off) is the floor. That is probably what you want, but "Omit to keep" in the parameter description does not say it, so an operator cannot tell inherit from unreachable.

On the handoff

The body is clear that no test in this PR has run anywhere yet and that revert proofs 1 to 5 are unrecorded. I read the tests as source; the finding on DarlingMcpAlertTools.RouteAdmin.cs:245 predicts TheLegacyWritesToken_SetsLevels1And3_AndEachIsJournaled_ButAnInvestigationsTokenSetsNone goes red in Darling PostgreSQL tests on its third assertion. That run is the check worth doing first — it decides whether the fix is in the store function or in the C# refusal.

claude[bot]
claude Bot previously requested changes Oct 23, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: CHANGES REQUESTED

Claims

  • VERIFIED — both route tools take the five agent fields and a DarlingMcpServedPolicy service parameter, so the client cannot say who it is — RouteAdmin.cs:69-74, 214-219; DI-bound at DarlingMcpHostService.cs:1316-1318.
  • VERIFIED — only operator-class tokens may use the fields, via new IsOperator/OperatorActor — DarlingMcpWritePolicy.cs:240-249; PolicyFor gives a named operator Effective(true, admin, false, name) (DarlingMcpTokenRoles.cs:68-70).
  • VERIFIED — the actor is operator:<name> or operator: plus the legacy fingerprint, never a token value — DarlingMcpWritePolicy.cs:247-249 over ReadCaller.cs:67-75 (8 hex of SHA-256, or the operator-chosen name).
  • VERIFIED — the function is SECURITY DEFINER, pinned search_path, no dynamic SQL, EXECUTE to mcp only — DarlingManagedRoles.cs:2766-2767, 2853, grant 1785; provision-roles.sql:1155, 1242, 1250.
  • VERIFIED — the two copies have the same text — the hunks differ only by {config} → config and the doubled {{1,64}}.
  • VERIFIED — the journal detail matches the Viewer's save — the nine keys and order at DarlingManagedRoles.cs:2843-2850 match NotificationRouteWriter.cs:775-786; before is JSON null on a create.
  • VERIFIED — a create journals only on a level, an edit only on a level or limit change — DarlingManagedRoles.cs:2837; v_changed excludes fallback_route_id, as commented.
  • VERIFIED — item 7 is agentTokenName ?? agentId at each pre-gate site — DarlingAgentActionGate.cs:70, DarlingMcpAgentActions.cs:162/202, DarlingMcpPlanForceTools.cs:220/887. The post-gate sites keeping action.AgentId (250, 428, 540, 558, 573) are not a gap: ServerActionGate.cs:74-80 refuses a mismatch first.
  • VERIFIED — item 10c names the entry's own setting — DarlingConfig.cs:514-530, message built at DarlingSecretSource.cs:125-127 and returned by DarlingMcpTokenRoles.cs:238-253.
  • VERIFIED — no rung needed; the CHECK already allows 0 to 3 — PgMigrations.cs V196 widens ck_config_notification_routes_agent with the same bounds the function re-checks.
  • VERIFIED — the journal trigger does not block it: route_agent_changed is in no InvestigationEvents.Kinds roster, no once_key is written, and the trigger returns early for the owner (DarlingManagedRoles.cs:3463-3466), which a definer body is.
  • VERIFIED — a level 3 server action still needs config.config_server_actions, which no MCP tool writes — its only mention under Mcp/ is the comment at ServerActionGate.cs:26.
  • VERIFIED — an investigations-only policy sets no agent field — IsOperator false, so AgentFieldRefusal returns AgentFieldsNeedOperator (RouteAdmin.cs:482-487).
  • UNVERIFIED — "builds with 0 errors on macOS" and "scrubcheck reports 0 hits" — both need the build and the tool run. (The body also says plainly no test here has run anywhere; I take that at face value.)
  • REFUTED — "The existing store functions still refuse a match, scope or destination change on an agent route for every caller. Part 2 opens only the agent fields." — config.update_notification_route refuses every column on a route at agent_level >= 1: DarlingManagedRoles.cs:2703-2705 / provision-roles.sql:1477-1479 raise DL403 on the row before any per-column check. With AgentRouteRefusal bypassed, a mixed update reaches that RAISE and is refused whole, agent fields included.
  • REFUTED — "TheLegacyWritesToken_…: … and it edits the agent route." — McpRouteAgentFieldsLiveTests.cs:151-153 sends send_resolutions: "always" to a route it just put at level 3; that adds a column (RouteAdmin.cs:307-311), so the function runs at RouteAdmin.cs:374, raises DL403, and RouteFunctionOutcome maps it to refused (RouteAdmin.cs:530-535). The assertion expects updated.

Riskiest lines

  1. Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.RouteAdmin.cs:245 — dropping the C# agent-route guard for an operator token while the store function's own guard is unchanged, so a mixed update is refused whole instead of writing the agent fields — covered by McpRouteAgentFieldsLiveTests.TheLegacyWritesToken_SetsLevels1And3_AndEachIsJournaled_ButAnInvestigationsTokenSetsNone, which fails on the defect at its send_resolutions assertion and passes on the fix
  2. Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.RouteAdmin.cs:326 — fallbackWithAgent decides, from a read outside the transaction, whether the fallback goes through update_notification_route or the new agent function; wrong either way it hits the DL403 above or is dropped from columns — UNCOVERED (no test moves an agent route's fallback over MCP; NotificationRouteFallbackLiveTests is the Viewer's path)
  3. Darling/PerformanceMonitor.Darling.Service/DarlingManagedRoles.cs:2808 — v_level NOT IN (0, 1, 2, 3) with the actor regex at 2790 is the whole guard on the one statement by which the mcp login writes the agent channel; the C# checks are advisory, since this function holds the row's lock — covered by McpRouteAgentFieldsLiveTests.AnOperatorToken_SetsLevel3_AndChangesARouteAtLevel3_EachJournaled

Traps near this diff

  • "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted (#3684). Alias, do not copy." — applies as a standing cost, not a defect here — the function is copied verbatim into DarlingManagedRoles.cs:2766 and provision-roles.sql:1142, and its journal JSON is a third copy of NotificationRouteWriter.Describe; the diff keeps all three in step, as the three existing route functions already do.
  • "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" — does not apply — Lite ships no route-authoring MCP tool to match (grep create_notification_route Lite/Mcp/*.cs is empty), and Lite/Mcp/McpAlertTools.cs:606-611 keeps AgentRouteRefusal for the only three route tools Lite serves, none touched here.
  • "Ruling (#5833): tests are load-bearing. … never answer a trap with a test that reads source text or asserts how code is spelled" — applies — McpRouteAgentFieldsLiveTests.cs:197 pins Assert.Equal(3, executes.Count) over a prefix match on ViewerGrantReplay.BatchStatements(); commented inline as non-blocking, since the fixture does not need the count.
  • "A test that encodes the lie … Ask what defect a test would fail ON" — applies in the author's favour — McpRouteAgentFieldsLiveTests.cs:153 is what caught the DL403 defect: it asserts the claimed capability rather than the code's behaviour.
  • "hours_back, limit, top, days_back are refused when out of range, never clamped" — does not apply, and is honoured — every agent limit is refused, never clamped (RouteAdmin.cs:494, and again under the lock at DarlingManagedRoles.cs:2811-2822); McpRouteAgentFieldsLiveTests.cs:118-123 asserts agent_max_per_hour: 500 answers invalid and leaves the stored 5.

Divergence (blind read vs PR body)

The two accounts agree everywhere except on how far the store functions refuse, and that gap is the finding.

The body says the functions "still refuse a match, scope or destination change on an agent route" and that part 2 "opens only the agent fields" — a reading under which an operator's non-agent columns keep working as today. The functions do not say that: config.update_notification_route refuses the row outright at agent_level >= 1 before it reads a column name. So the bypass at RouteAdmin.cs:245 does not open the agent fields beside the old behaviour; on a route at level 1 or more it turns every mixed update into a DL403 that writes nothing, with a message ("it is changed in the Viewer, not over MCP") that contradicts the tool's own new description. The body then cites TheLegacyWritesToken_… as proof those other fields "are no longer refused" — the diff refuses them.

Two smaller divergences, posted as comments: the new paragraph at DarlingMcpAlertTools.cs:969-970 asserts "level 3 stays person-only", contradicted by the body's own level-3 decision and by the function's (0, 1, 2, 3); and Darling/README.md:867 still tells operators "an agent route is changed only in the Viewer" and names three store functions where there are now four, which the body does not mention.

I looked for a widening the body might have understated and found none: delete_notification_route, set_notification_route_enabled and set_notification_route_fallback still call AgentRouteRefusal with no actor, match/scope/destinations stay refused on any route with an agent setting, and level 3 buys no server action without config.config_server_actions.

Reviewer checklist

  • Run Darling PostgreSQL tests with DARLING_TEST_PG set and read TheLegacyWritesToken_SetsLevels1And3_AndEachIsJournaled_ButAnInvestigationsTokenSetsNone's third assertion — it decides whether the fix belongs in the function's DL403 or in the C# refusal.
  • Record revert proofs 1 to 5 from the body; none has run, so no claim about what each test fails on is confirmed yet.
  • On a self-managed store, re-run tools/provision-roles.sql and confirm config.set_notification_route_agent exists with EXECUTE to mcp before an operator token calls the tool — without it the call lands in McpHelpers.FormatError (42883 is not a DL code) rather than in a sentence an operator can act on.
  • With a real MCP client, confirm the journal actor is operator:<name> for a named operator token and operator:token <8 hex> for the legacy one — ReadCaller.AmbientMcpToken is set only by the tool filter.

…ames a match change (#6542 part 2, review round 1)

M2: config.update_notification_route and config.set_notification_route_destinations take the actor the
service passes. They change a route with an agent setting only for an operator: actor and refuse every
other caller with DL403, as set_notification_route_agent does. The old signatures are dropped in the
managed batch and in tools/provision-roles.sql, so neither stays callable. set_notification_route_enabled
and set_notification_route_fallback hand an operator token's change to an agent route to the same path.

L2: update_notification_route returns the row before the change, and set_notification_route_agent journals
it as the before, so one call that changes metric_match and agent_level journals both, as the Viewer does.

L3: a theory over the kill and PostgreSQL kill refusals made before the gate, and one tools/call through
the host with a named agent token, each asserting the journal row names the token.

L1 and M1: the test comment and the ServerActionGate comment now say what holds.
… review round 1)

"v_changed || 'agent_level'" reads the literal as a text array and fails with
"malformed array literal", so every journaled agent change rolled back. Checked against a
throwaway PostgreSQL 16 container.
@claude

claude Bot commented Oct 23, 2026

Copy link
Copy Markdown

Follow-up review of the 3 commits in 5a20f5b..5d6788b (review round 1 fixes for M2/L2/L3 plus the array_append runtime fix).

The array_append fix (5d6788b) is real and correct. The prior v_changed := v_changed || 'agent_level' pattern hits PostgreSQL's well-known operator-resolution gotcha: with an unknown-type string literal on the right of ||, the planner prefers the anyarray || anyarray overload and tries to parse 'agent_level' as an array-literal string, raising "malformed array literal" — which would roll back every journaled agent change. array_append(v_changed, 'agent_level') sidesteps the ambiguity entirely. Verified identical fixes landed in both DarlingManagedRoles.cs and tools/provision-roles.sql (parity held).

M2 (update_notification_route / set_notification_route_destinations take p_actor, DROP+CREATE with new signatures, GRANTs updated to new arities). Checked every DROP FUNCTION IF EXISTS against the actual prior signature (base commit 5a20f5b) — they match exactly, so no orphaned overload survives. Parameter counts on every CREATE/REVOKE/GRANT line (9 for update_notification_route, 11 for set_notification_route_agent, 8 for set_notification_route_destinations) are internally consistent between DarlingManagedRoles.cs and provision-roles.sql.

L2 (p_before round-trip for journaling a match/scope/enabled change made in the same call as an agent change). Traced journalsMatchOrFlag in DarlingMcpAlertTools.RouteAdmin.cs:333 through to SetRouteAgentAsync's operatorActor! null-forgiving use at line ~398 — safe, because journalsMatchOrFlag can only be true when operatorActor is not null, and the agent.Any && operatorActor is null early-return (above) guarantees the same for the other disjunct. Also checked that a route whose agent_level is already set is guaranteed (by set_notification_route_agent's own bounds checks) to have all four limit columns populated, so the re-entrant call with p_before set and no agent columns changed can't spuriously hit the "Requests per hour must be between 1 and 60" exception on a previously-Off route.

L3 (new identity-journaling tests in AgentTokenIdentityGateLiveTests.cs). No production code changed here (DarlingMcpPlanForceTools.cs is untouched in this range) — agentTokenName was already threaded through RunKillAsync/RefuseBeforeGateAsync before this range started, so these are legitimate new regression tests pinning already-correct behavior, not claims of a new fix. Call sites and signatures (RunKillAsync, KillToolName, BuildServer, ToolsCallAsync) all check out against current definitions.

No defects found in these three commits. The PR body's claims about this round (M2, L2, L3, and the array_append discovery) all match the diff.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: LGTM

Claims

  • VERIFIED — "set_notification_route_agent appends with array_append... fails with 'malformed array literal'" — confirmed the prior v_changed || 'agent_level' pattern in the base commit and the fix to array_append(v_changed, 'agent_level') in both DarlingManagedRoles.cs:2854-2865 and tools/provision-roles.sql (same text, parity held)
  • VERIFIED — "M2: config.update_notification_route and config.set_notification_route_destinations take the actor the service passes... The old signatures are dropped" — DROP FUNCTION IF EXISTS lines match the exact prior signatures from commit 5a20f5b, and every GRANT/REVOKE arity was updated to match (9/11/8 params respectively)
  • VERIFIED — "set_notification_route_enabled and set_notification_route_fallback hand an operator token's change to such a route to the same path" — both call UpdateNotificationRouteAsync directly when existing.AgentLevel is set and served?.Policy.OperatorActor is present (DarlingMcpAlertTools.cs:792, :859), argument counts match UpdateNotificationRouteAsync's 16-parameter signature
  • VERIFIED — "L2: update_notification_route returns the row before the change, and set_notification_route_agent journals it as the before, so one call that changes metric_match and agent_level journals both" — traced the before/p_before plumbing end to end (RouteAdmin.cs:367 through set_notification_route_agent's p_before handling) and the new test OneCall_ChangingTheMatchAndTheLevel_JournalsBoth_WithTheOldMatchInBefore asserts exactly this
  • VERIFIED — "L3: a theory over the kill and PostgreSQL kill refusals made before the gate... each asserting the journal row names the token" — new tests in AgentTokenIdentityGateLiveTests.cs call existing, unchanged RunKillAsync/RefuseBeforeGateAsync paths that already thread agentTokenName (pre-dating this range), so these are regression tests for already-correct behavior, consistent with "no production file in DarlingMcpPlanForceTools.cs changed in this diff"
  • UNVERIFIED — "Checked against a throwaway PostgreSQL 16 container" and "CI's Windows build and Darling PostgreSQL tests are the only full runs" — a live DB/CI run cannot be confirmed from a read-only tree

Riskiest lines

  1. Darling/PerformanceMonitor.Darling.Service/DarlingManagedRoles.cs:2854-2865 — the array_append fix itself; a regression back to || silently breaks every agent-route journal write — covered by OneCall_ChangingTheMatchAndTheLevel_JournalsBoth_WithTheOldMatchInBefore and AnOperatorToken_ChangesSendResolutionsAndEnabled_OnALevel3Route_AndTheStoreRefusesAnyOtherActor (both exercise the v_changed append paths and assert the journal row exists)
  2. Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.RouteAdmin.cs:333,398 — operatorActor! null-forgiving dereference inside if (agent.Any || journalsMatchOrFlag); a future change that lets journalsMatchOrFlag go true with a null operatorActor would NRE — covered by AnOperatorToken_ChangesSendResolutionsAndEnabled_OnALevel3Route_... (exercises the enabled-only branch with agent.Any false)
  3. Darling/PerformanceMonitor.Darling.Service/DarlingManagedRoles.cs:2712,2718,2723 — the NOT v_operator additions on the three agent-route DL403 guards in update_notification_route; a dropped NOT v_operator either direction either locks operators out or opens agent routes to any actor — covered by AnOperatorToken_ChangesSendResolutionsAndEnabled_OnALevel3Route_AndTheStoreRefusesAnyOtherActor (asserts both the operator success and the DL403 refusal for NULL/agent: actors)

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" — does not apply: this follow-up only touches Darling-only files (Postgres SECURITY DEFINER functions, MCP route-admin tools, operator-token identity tests); Lite's McpAlertTools.cs:606 AgentRouteRefusal has no operatorActor parameter at all, confirming Lite never had the operator-exception feature this PR extends, so there is no counterpart to drift from in this range
  • Array-literal || ambiguity (not a named trap in REVIEW-TRAPS.md, but the exact defect this round's third commit fixes) — applies to the two files it was fixed in and nowhere else was found using the same var || 'literal' shape in this diff (checked both DarlingManagedRoles.cs and provision-roles.sql for the pattern post-fix: none remain)

Divergence (blind read vs PR body)

None — the body's account of this round (M2, L2, L3, and the array_append discovery) matches the diff on every claim graded above.

Reviewer checklist

  • Run the live-PostgreSQL Darling.Tests suite (McpRouteAgentFieldsLiveTests, AgentTokenIdentityGateLiveTests) in CI to confirm the array_append fix and the new L2/L3 tests actually pass against a real server, since this tree is read-only

One conflict, in DarlingMcpHostGateLiveTests.cs: this branch's new
NamedTokensThatCannotBeServed_WithNoLegacyToken_StopTheStart test is kept,
and the source-text test TryStartServerAsync_ResolvesTheLoopbackOnlyToken,
which dev removed in #6507, stays removed.
ScalarAsync turns a boolean into "True", so the enabled and send_resolutions
asserts compared "true" with "True" and failed (Darling PG tests shard 1).
Both reads now cast to text, as the test's other reads do. The send_resolutions
read was after the failing line, so it had not run yet.
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
Moved out of this round: the create-path set_notification_route_agent call,
the level-3 refusal and the legacy OperatorActor. They change tests this
round's other reverts target, so they run in rounds 3 and 4.

- DarlingManagedRoles.cs and provision-roles.sql: AND NOT v_operator dropped from the agent-route DL403 guard
- DarlingMcpPlanForceTools.cs: RefuseBeforeGateAsync passes parsed.AgentId
- DarlingMcpHostService.cs: return false after the named-token Critical line deleted
- DarlingMcpAlertTools.RouteAdmin.cs: the update-path SetRouteAgentAsync passes no before
- DarlingMcpPlanForceTools.cs: agentTokenName dropped at the bad-session, bad-login-time, Azure-no-database, PG-bad-backend-id and PG-pid-mismatch refusals
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
… (do not merge)

- DarlingMcpWritePolicy.cs: the legacy-token branch of OperatorActor returns TokenName alone
…ields

# Conflicts:
#	Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.cs
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
- DarlingMcpAlertTools.RouteAdmin.cs: CreateNotificationRouteAsync no longer calls SetRouteAgentAsync
- DarlingMcpAlertTools.RouteAdmin.cs: AgentFieldRefusal refuses level 3 again
- DarlingMcpWritePolicy.cs: OperatorActor returns TokenName alone for the legacy token
- DarlingMcpPlanForceTools.cs: RefuseBeforeGateAsync journals parsed.AgentId, not agentTokenName
- DarlingMcpHostService.cs: return false removed after the named-token Critical line
- DarlingManagedRoles.cs and provision-roles.sql: AND NOT v_operator dropped from the agent-route DL403 guard
- DarlingMcpAlertTools.RouteAdmin.cs: SetRouteAgentAsync call in the update path no longer passes before
- DarlingMcpPlanForceTools.cs: agentTokenName dropped at the kill bad-session, bad-login-time, Azure database, PG bad backend id and PG pid mismatch refusal sites

(cherry picked from commit 4e8b98c)
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
Moved out of this round: the create-path set_notification_route_agent call,
the level-3 refusal and the legacy OperatorActor. They change tests this
round's other reverts target, so they run in rounds 3 and 4.

- DarlingManagedRoles.cs and provision-roles.sql: AND NOT v_operator dropped from the agent-route DL403 guard
- DarlingMcpPlanForceTools.cs: RefuseBeforeGateAsync passes parsed.AgentId
- DarlingMcpHostService.cs: return false after the named-token Critical line deleted
- DarlingMcpAlertTools.RouteAdmin.cs: the update-path SetRouteAgentAsync passes no before
- DarlingMcpPlanForceTools.cs: agentTokenName dropped at the bad-session, bad-login-time, Azure-no-database, PG-bad-backend-id and PG-pid-mismatch refusals

(cherry picked from commit 8452948)
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
- DarlingManagedRoles.cs and provision-roles.sql: the agent-route DL403 block in update_notification_route deleted

(cherry picked from commit e003cda)
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
… (do not merge)

- DarlingMcpWritePolicy.cs: the legacy-token branch of OperatorActor returns TokenName alone

(cherry picked from commit e246365)
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
- DarlingMcpAlertTools.RouteAdmin.cs: the create-path set_notification_route_agent call removed
- DarlingMcpAlertTools.RouteAdmin.cs: AgentFieldRefusal refuses level 3 again

(cherry picked from commit e692aa1)
@erikdarlingdata
erikdarlingdata dismissed claude[bot]’s stale review October 23, 2026 14:00

Claude posting for Erik Darling. Stale review. It was written at 5a20f5b. Its two refuted claims (the store refused every change on an agent route, and the legacy-token test did not edit one) were fixed in review round 1 (M2). The bot's re-review 5625028501 at 5d6788b is LGTM. CI is green at 8b60a0b.

@erikdarlingdata
erikdarlingdata merged commit 6eed859 into dev Oct 23, 2026
33 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/6542-route-agent-fields branch October 23, 2026 14:00
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
…ature

#6593 added the actor to config.set_notification_route_destinations too, so the
owner list still asked for its old 7-argument signature. Every entry in the list
is now checked against the script's own definitions: each names a function the
script creates and does not drop before step 8.
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
…ed (#6644)

* Fix the store script's owner list for the two route functions #6593 changed

Step 8 of provision-roles.sql (#6613) hands each definer-rights function to the
owner of config.config_command. It named update_notification_route by its old
8-argument signature. #6593 dropped that signature and created a 9-argument one
with the actor. The two PRs merged in that order without a CI run on both, so
the script now stops at step 8 with "function ... does not exist".

The list now names the 9-argument signature, and set_notification_route_agent,
the definer-rights function #6593 added.

* Step 8 also names set_notification_route_destinations by its new signature

#6593 added the actor to config.set_notification_route_destinations too, so the
owner list still asked for its old 7-argument signature. Every entry in the list
is now checked against the script's own definitions: each names a function the
script creates and does not drop before step 8.
erikdarlingdata added a commit that referenced this pull request Oct 23, 2026
…stores the agent fields (L5 of #6593) (#6639)

update_notification_route takes clear_agent_level true for an operator token. It sets agent_level to NULL (Inherit) and keeps the four limits, as the Viewer's route window saves an Inherit. agent_level and clear_agent_level together are invalid. The store function already accepts a NULL level, so it is unchanged.

The undo of an operator token's update_notification_route call now carries agent_level and the four limits, or clear_agent_level true when the level was Inherit. Agent fields that already hold the stored value are not written, so an undo changes only what differs. set_notification_route_enabled and set_notification_route_fallback hand an operator token's change on an agent route to update_notification_route, so their answers carry the same undo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant