Repository navigation
Let the executor own the MCP confirmation gate (#599) - #534
Conversation
Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Run failed. View the logs →
|
|
REQUEST CHANGES, head Pullfrog could not run this review. Codex and Pullfrog share one OpenAI meter, exhausted until 2026-09-22. A separate agent reviewed in the worktree carrying this exact head, and the orchestrator re-checked the blocking finding by hand. The substitution is named so nobody later reads this as having had the usual reviewer.
The core change is right. The executor does own the gate now, the gate fails closed, and no second path reaches a confirmed operation without it. This blocks on one false load-bearing claim in the body and on a hole in the guard test that carries the whole safety argument for the removed gate. What ran
Whole suite at this head, 6251 tests, zero failures. The body's numbers are exact. The fix was broken on purpose and the tests went red. Deleting only the capability skip at CI: 23 checks, all pass except The five security questions, answered by driving themDoes the executor own the gate? Yes. Every confirmation-gated capability was enumerated from Flag OFF, which is production today: the change is NOT inert, and the body does not claim it is. Flag ON: Replay, reuse, expiry: none. The confirmed retry run twice with one token returns the result, then Failure mode: fail closed. P1, and it is why this is REQUEST CHANGESThe body says twelve
}, confirmationToken: null, cancellationToken);Re-checked by hand at this head: Their capability is Failure scenario: a person asks an MCP client to log ten habits. The client calls The body states "They share the one call site, so they are fixed by the same change." That is false for these two. The body lands in history on squash, and a later reader will cite it as proof that MCP confirmation is complete. Fix it here, under "Maximum implementation": add the parameter to both methods and thread it through P2, both worth fixing in this pull requestThe guard test does not pin the operation the tool forwards. Concrete: Two smaller notes on that file: A second route into P3
Body claims checked and found trueThree capabilities declare Deploy orderSafe to deploy before One consequence a human must know before the flip, independent of the order. Once the row is One more note on the flip itself, which belongs to |
bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Run failed. View the logs →
|
1 similar comment
|
Run failed. View the logs →
|
|
APPROVE, head Pullfrog could not run this review. Codex and Pullfrog share one OpenAI meter, exhausted until 2026-09-22. A separate agent reviewed in the worktree carrying this exact head. The substitution is named so nobody later reads this as having had the usual reviewer.
The P1 is fixed, driven rather than readA driver in the extract stands up the real graph: The middleware stepped aside on every call, asserted on the JSON-RPC body carrying no The new guard is real, and every named mutation reddens itThe guard reads the tool sources off disk, so no rebuild is needed between mutations.
The messages are specific rather than generic: Two things worth naming. On the operation-id mutation, The guard also resolves the same thing the executor gates on, checked rather than assumed: The real tree still passes
6255 total, zero failures, up 4 on Infrastructure from round 1's 2241, matching three new guards plus
The ten remaining
|
…rameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Run failed. View the logs →
|
1 similar comment
|
Run failed. View the logs →
|
|
REQUEST CHANGES, head Pullfrog could not run this review. Codex and Pullfrog share one OpenAI meter, exhausted until 2026-09-22. A separate agent reviewed in the worktree carrying this exact head. The substitution is named so nobody later reads this as having had the usual reviewer.
Every item round 3 was ordered to fix is fixed. All five named mutations redden, the P3 swap reddens, and the four body and doc items are correct at this head. This is REQUEST CHANGES for a different reason: five further mutations each compile, each restore the exact The ordered mutations, and the P3 re-checkGuard command throughout, in the extract:
M1 to M4 name both bulk tools and print the exact offending expression. M6 prints Five that do leave it greenEvery one of these also builds:
And the four that failed to break it, which is the evidence the rule is general rather than a list of five: P2, any
|
…599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Run failed. View the logs →
|
1 similar comment
|
Run failed. View the logs →
|
|
REQUEST CHANGES, head Pullfrog could not run this review. Codex and Pullfrog share one OpenAI meter, exhausted until 2026-09-22. A separate agent reviewed in the worktree carrying this exact head. The substitution is named so nobody later reads this as having had the usual reviewer.
The five ordered bypasses all redden
All six earlier mutations still redden too: The real tree is unchanged at 32 / 585 / 3393 / 2245, 6255 passed, zero failures, so the guard does not refuse everything. The three legitimate shapes behave as documented: a direct-only call is green, exactly one same-file helper hop is green, and two hops redden on the routing guard, which fails closed. Nine of thirteen new attacks failed against the new codeWorth recording, because these are the evidence the fixes are general rather than a list:
P2. The assignment rule misses every compound assignment
[GeneratedRegex(@"\b" + ConfirmationTokenParameterName + @"\s*=[^=>]")]
Failure scenario, and it is not sabotage. A later change to The doc comment at One alternation fixes it, checked against every operator in the ordered list: [GeneratedRegex(@"\b" + ConfirmationTokenParameterName + @"\s*(?:\?\?|\+)?=[^=>]")]
P2.
|
The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Run failed. View the logs →
|
1 similar comment
|
Run failed. View the logs →
|
|
|
@pullfrog review |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Reviewed the full confirmation-gate change across the MCP middleware, executor, affected tools, audit flow, and regression tests.
-
Confirmation ownership: Confirmation-gated MCP calls now bypass the legacy middleware policy pass and rely on the token-aware
IAgentOperationExecutorevaluation. -
Token recovery: The bulk log and skip tools now accept and forward confirmation tokens, while step-up guidance names the complete recovery sequence.
-
Audit continuity: Unknown executor operations now emit a denied audit entry before returning the unsupported response.
-
Safety coverage: Behavioral tests exercise refusal, step-up recovery, token rejection, and API-key closure, while catalog-backed guards cover every gated MCP tool's route, capability, and token forwarding.
GPT Sol | 𝕏
* Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: resolve stored recap week anchors without the current preference A closed week anchor lives in the recap share link and in the stored snapshot key. Validating it against today's WeekStartDay rejected a Monday anchor after the person moved to Sunday, before the stored snapshot could be read. Accept any supported week start (Sunday or Monday) for an explicit closed anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…20) (#532) * Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Redesign the transactional emails, AI push copy and error messages Closes thomasluizon/orbit-tickets#75 (R20). Emails. All 7 types plus the shared wrapper move to the granted canon: the violet #7F46F7 becomes the warm orange #C4530F, the gradient header band and every CTA glow are gone (DESIGN.md bans both), Rubik and Inter become the Geist Sans and Geist Mono stacks, and the planet emoji and sparkle bullets give way to the mark and a plain numbered list. Emails now author light and override dark through prefers-color-scheme, which matches "light mode MANDATORY, dark primary" and survives a forced inversion far better than the dark only document did. Copy is rewritten in both languages to say what happened before what to do next. Support copy moves out of its template into EmailCopy so markup holds no strings. AI push copy. The slip alert, proactive check in and daily summary prompts demonstrated an exclamation mark and a doubled hyphen inside their own worked examples, and at temperature 0.9 the model reproduced both. One shared voice contract, NotificationVoice, now states each ban and carries no example at all. The fallback strings carried the same two tells and are rewritten. Error messages. A domain guard firing is an ordinary event, but its message is the sentence a developer wrote, and on 2026-08-05 a tester read "Days can only be set when frequency quantity is 1." verbatim, in English, inside a pt-BR interface. ErrorCopy now maps every error code to user facing copy in both languages, and one result filter swaps the message at the response boundary. DomainErrors stays developer facing, as the ticket requires. The catalog is total over ErrorCodes, ErrorMessages and DomainErrors, and a reflection test fails the build when a constant arrives without copy, so the raw message fallback is unreachable. ErrorMessages now takes its English from the same catalog rather than repeating 145 sentences in a second file. AppError and Result carry the arguments behind a {0} placeholder so the localized template can be formatted with the same values, which the eagerly formatted message alone no longer exposed. Also removed: five pay gate strings ending in "Upgrade to unlock!", a banned word and an exclamation on a sell; inlined copies of error sentences in three chat tools; and a hardcoded 500 envelope message. Tests: 6980 passing, 0 failing. Line coverage 86.40%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Build the banned dashes from code points and refresh the architecture map Two red checks on PR 532 at c50f052. Dash Ban. The three new voice tests detected a banned dash by embedding one, and the gate scans a test file like any other. The two constants are now built from their code points, ((char)0x2014) and ((char)0x2013), so no literal character sits in the source. The escape form these files were written with did not survive: the lefthook pre-commit step runs dotnet format, which normalized "—" back into the raw character before the commit landed. A code point arithmetic expression has no literal for a formatter to normalize. The assertions are unchanged. Verified by injecting a real em dash into one ErrorCopy entry and one EmailCopy string: EveryEntryObeysTheErrorVoice and NoEmailStringCarriesADashCharacter both went red, then green again once the injection was reverted. drift. architecture.json and architecture.html were stale against the new test classes and the changed file counts. Regenerated with node tools/arch-map.mjs and committed here, in the change that requires them. The generator is deterministic: two consecutive runs are byte identical. tools/dash-baseline.json is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix the four blocking findings on the email and error copy pass C1: the deletion email promised a permanent wipe while the product only deactivates. The intro now states the grace window, read from AppConstants.MaxDeletionGraceDays, and that signing in again cancels it. C2: the copy pass had moved the wrong-code failure onto a new error code both shipped clients have no branch for, which sends Orbit 1.3.31 and the web step-up screen to a generic error. The code returns to INVALID_VERIFICATION_CODE, and the count rides in a counted copy variant that keeps the trailing token those clients parse. C3: the reply language came from Accept-Language alone, which neither the Android app nor the web Server Actions send, so every signed-in pt-BR reader got English. RequestLanguageResolver reads the stored User.Language first and keeps the header as the anonymous fallback. The header parse now honours quality values. C4: the slip alert claimed the usual time had arrived, while the scheduler sends two hours early and at 08:00 when no peak hour exists. The prompt states when the push lands and the fallback makes no time claim without a peak hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the copy claims match the code that produces them C5: the check-in fallback called one open habit "a few" and promised that Astra records the rest. The scheduler sends as soon as one habit is off track, and LogHabitTool records only the habit a person names. C6: the 429 body was an anonymous object the localizing filter could not match, so the RATE_LIMITED copy was unreachable. It is built from ErrorMessages.TooManyRequests now, which also gives the 429 an error code for the first time. Retry-After and the request id already ride in headers. C8: the sync copy promised a reload and a later send that no caller performs. C9: a challenge does not take part, people do. C10: the recap arrives only for a reader with a push subscription, so the copy points at coming back instead. D1 and D4: two doc comments claimed more than the code does. The filter covers ErrorResponse bodies only, and a FluentValidation message never reaches it. A missing catalog entry throws on the first request, not at startup. D3: BuildSummaryPrompt is internal and joins both voice theory sources, so the guard written for its doubled-hyphen leak now reaches it. D5: AppError compared Args by reference, so two errors formatted with the same count came out unequal. Equality walks the arguments now. The three voice tests that read NotificationVoice.Rules' own literals back out of it now assert the bans over every prompt a model receives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Refresh the architecture map for the review round Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Refactor locale selection and notification fallbacks * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: drop the duplicate workflow concurrency keys the merge-forward created Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…531) * Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Resolve every stored colour scheme to the granted accent The design decision that granted one warm orange accent deleted colour as data, but the API still reported whichever of the six historical schemes an account had stored. An account carrying "rose" rendered rose. This is the deploy-API-first half, and it is append-only. The colorScheme field stays in every DTO, the set_color_scheme operation stays, the six values stay writable, and no migration touches a stored row. Only the read paths change: GetProfileQuery, ExportUserDataQuery and the Astra context snapshot now report ColorSchemes.Granted instead of the stored value, so an existing account renders warm orange on a shipped client with no app update. The stored column becomes write-only on purpose. Keeping the writes means reverting this commit restores the old behaviour with every value intact. The two agent tools stopped echoing the requested value back, because reporting "rose" to a model that then tells the user their accent is rose is now false. Their descriptions say the same thing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Collapse the colour scheme write and keep the export truthful Round 2 review on pull request 531 found the read and the write disagreeing. GetProfileQuery answered the granted accent while SetColorSchemeCommand still stored the requested key, so a shipped client's tap stored "blue", refetched "orange" and snapped back. User.SetColorScheme now still accepts every historical key, so an old client never sees a 400, and stores ColorSchemes.Granted instead of the request. A null request still clears the preference, because no read surfaces the stored value and writing one on a clear would only add data nobody reads. ExportUserDataQuery goes back to user.ColorScheme. A subject-access export answers what the database holds, and no migration rewrites rows written before the collapse, so those rows still report their historical key. AgentContextSnapshot drops ColorScheme. Every account resolved to the same constant, so the prompt line carried no information and the nullable field could no longer be produced. ColorSchemes.AcceptedValues becomes a FrozenSet so the domain validation rule cannot be rewritten through the public static reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the agent catalog agree with the data export The data catalog told Astra that every account reads back the same colour scheme, while the export on the same field returns the raw column on purpose. A row written before the collapse still holds its own key, so the two surfaces answered one question two ways. The catalog now states both halves: the profile reads back the granted value and a pre-collapse row keeps its original key. The sweep it asked for found no twin carrying the same claim, and a test over every catalog entry, capability, surface, chat tool and MCP tool description now keeps it that way. Three write-side sentences said "store the user's colour scheme preference" while the write stores the granted value. They now say the value is accepted and the stored one becomes the granted one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: drop the duplicate workflow concurrency keys the merge-forward created Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Recover concurrent habit log duplicates (#325) (#552) * Start #325 habit log duplicate recovery * Recover duplicate habit logs without losing idempotency response * chore(deps): bump the github-actions group across 1 directory with 5 updates (#530) * chore(deps): bump the github-actions group across 1 directory with 5 updates Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` | | [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` | Updates `actions/upload-artifact` from 5 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v5...v7) Updates `github/codeql-action/init` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `actions/setup-java` from 5 to 6 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](actions/setup-java@v5...v6) Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17 - [Release notes](https://github.com/oasdiff/oasdiff-action/releases) - [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/init dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: oasdiff/oasdiff-action/breaking dependency-version: 0.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> * fix: restore optional response removal gate (#664) * fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ORB-101: verify Turnstile tokens on anonymous writes (#550) * chore: open ORB-101 review branch * feat: gate anonymous writes with optional Turnstile verification * test: cover Turnstile gate and verifier failures * test: assert valid Turnstile request returns success * test: ground Turnstile responses in observed Siteverify results Document web, Android, and landing activation checks for ORB-101. * Fix Astra goal access for every plan (#677) (#560) * fix: accept stored recap week anchors after preference changes (#683) (#559) * fix: resolve stored recap week anchors without the current preference A closed week anchor lives in the recap share link and in the stored snapshot key. Validating it against today's WeekStartDay rejected a Monday anchor after the person moved to Sunday, before the stored snapshot could be read. Accept any supported week start (Sunday or Monday) for an explicit closed anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: use closed week anchor for recap cache misses --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Prune idle Hangfire session connections for #564 (#554) * Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558) --- updated-dependencies: - dependency-name: Scalar.AspNetCore dependency-version: 2.17.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Make unsubscribe token tampering test byte-accurate (#673) (#561) * Make goal completion timestamp test deterministic (#676) (#562) * Start #676 goal clock fix * Make goal completion timestamp test deterministic (#676) * chore: regenerate the architecture map for the goal clock test The test now references HabitLog, so node tools/arch-map.mjs adds it to the test's references. Two runs produce the same output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537) * Pin Pullfrog reviewer model and effort for #636 * Set Pullfrog effort to medium on Thomas's decision (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Describe the pinned Pullfrog reviewer in the workflow comment (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort input was dropped. The review step now runs the action's own code at pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a medium rung. Return to @v0 once 0.1.83 is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Take Pullfrog model and effort from the repository config (#565) The app dispatch payload carries model and effort and outranks action inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537 never applied (run 36187465385 resolved effort xhigh). Effort is now 0.25 (medium on GPT-6 Sol) in the Pullfrog repo config. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix relative reminders across local day boundaries (#566) * chore: start reminder boundary fix for #695 * test: inject a fixed clock into relative reminder checks * fix: send relative reminders on their local occurrence day * chore: ratchet the scheduler suppression count and regenerate the map #695 removed the two relative-reminder UTC-date suppressions, so the closed allowlist now declares 2 ORBIT0004 sites in ReminderSchedulerService.cs. The architecture map is regenerated for the new clock dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: fire relative reminders at the first local due time across DST Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is ambiguous and ConvertTimeToUtc picked the later occurrence, an hour after the old scheduler. A repeated hour now resolves to its first occurrence, and a due time inside a spring-forward gap resolves to the moment the clock jumps past it instead of being skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix bulk habit replay idempotency (#567) * chore: open ticket 389 review early * fix: make bulk habit writes replay safe * fix: scope idempotency ledger by command content * fix: scope idempotency ledger by request ordinal * Use published Pullfrog v0 for Codex review (#568) * Start #700 Pullfrog action update * Use published Pullfrog v0 for Codex review * ci: pin both Pullfrog steps to the v0 release commit SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency. Pin the commit the v0 tag named on 2026-09-26, release 0.1.83. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs: align worker PR handoff with ticket orders (#570) * Fix child habit title messages on update for #651 (#572) * Fix explicit culture formatting for #704 (#569) * chore: begin #704 culture formatting work * fix: format #704 notification prompts logs and goal tools explicitly * fix: enforce invariant formatting across #704 prompts and machine text * test: cover invariant decimal formatting for #704 * chore: refresh architecture artifacts for #704 * test: cover remaining culture formatting paths for #704 * Fix last completion date after habit type changes (#574) * fix: preserve habit log slip kind at write time * test: include slip kind in concurrent log fixture * fix: classify old habit logs during rolling deploys * Clean stale text in API repository (#575) * Use neutral names in test fixtures * Make repository text timeless * Remove disallowed narration comment * Add timeless text gate to API repository (#576) * Add timeless text gate to API repository * fix: read comments in XML build files * fix: match script end tags with attributes * fix: end a script only at a real end-tag delimiter * Prepare timeless cleanup for #715 * Preserve main culture formatting in merged code * fix: sync the checker's quote and staged-blob fixes * fix: let a reviewed directory entry exempt any rule but machine-path * fix: sync the final timeless checker --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Recover concurrent habit log duplicates (#325) (#552) * Start #325 habit log duplicate recovery * Recover duplicate habit logs without losing idempotency response * chore(deps): bump the github-actions group across 1 directory with 5 updates (#530) * chore(deps): bump the github-actions group across 1 directory with 5 updates Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` | | [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` | Updates `actions/upload-artifact` from 5 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v5...v7) Updates `github/codeql-action/init` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `actions/setup-java` from 5 to 6 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](actions/setup-java@v5...v6) Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17 - [Release notes](https://github.com/oasdiff/oasdiff-action/releases) - [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/init dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: oasdiff/oasdiff-action/breaking dependency-version: 0.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> * fix: restore optional response removal gate (#664) * fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ORB-101: verify Turnstile tokens on anonymous writes (#550) * chore: open ORB-101 review branch * feat: gate anonymous writes with optional Turnstile verification * test: cover Turnstile gate and verifier failures * test: assert valid Turnstile request returns success * test: ground Turnstile responses in observed Siteverify results Document web, Android, and landing activation checks for ORB-101. * Fix Astra goal access for every plan (#677) (#560) * fix: accept stored recap week anchors after preference changes (#683) (#559) * fix: resolve stored recap week anchors without the current preference A closed week anchor lives in the recap share link and in the stored snapshot key. Validating it against today's WeekStartDay rejected a Monday anchor after the person moved to Sunday, before the stored snapshot could be read. Accept any supported week start (Sunday or Monday) for an explicit closed anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: use closed week anchor for recap cache misses --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Prune idle Hangfire session connections for #564 (#554) * Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558) --- updated-dependencies: - dependency-name: Scalar.AspNetCore dependency-version: 2.17.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Make unsubscribe token tampering test byte-accurate (#673) (#561) * Make goal completion timestamp test deterministic (#676) (#562) * Start #676 goal clock fix * Make goal completion timestamp test deterministic (#676) * chore: regenerate the architecture map for the goal clock test The test now references HabitLog, so node tools/arch-map.mjs adds it to the test's references. Two runs produce the same output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537) * Pin Pullfrog reviewer model and effort for #636 * Set Pullfrog effort to medium on Thomas's decision (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Describe the pinned Pullfrog reviewer in the workflow comment (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort input was dropped. The review step now runs the action's own code at pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a medium rung. Return to @v0 once 0.1.83 is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Take Pullfrog model and effort from the repository config (#565) The app dispatch payload carries model and effort and outranks action inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537 never applied (run 36187465385 resolved effort xhigh). Effort is now 0.25 (medium on GPT-6 Sol) in the Pullfrog repo config. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix relative reminders across local day boundaries (#566) * chore: start reminder boundary fix for #695 * test: inject a fixed clock into relative reminder checks * fix: send relative reminders on their local occurrence day * chore: ratchet the scheduler suppression count and regenerate the map #695 removed the two relative-reminder UTC-date suppressions, so the closed allowlist now declares 2 ORBIT0004 sites in ReminderSchedulerService.cs. The architecture map is regenerated for the new clock dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: fire relative reminders at the first local due time across DST Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is ambiguous and ConvertTimeToUtc picked the later occurrence, an hour after the old scheduler. A repeated hour now resolves to its first occurrence, and a due time inside a spring-forward gap resolves to the moment the clock jumps past it instead of being skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix bulk habit replay idempotency (#567) * chore: open ticket 389 review early * fix: make bulk habit writes replay safe * fix: scope idempotency ledger by command content * fix: scope idempotency ledger by request ordinal * Use published Pullfrog v0 for Codex review (#568) * Start #700 Pullfrog action update * Use published Pullfrog v0 for Codex review * ci: pin both Pullfrog steps to the v0 release commit SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency. Pin the commit the v0 tag named on 2026-09-26, release 0.1.83. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs: align worker PR handoff with ticket orders (#570) * Fix child habit title messages on update for #651 (#572) * Fix explicit culture formatting for #704 (#569) * chore: begin #704 culture formatting work * fix: format #704 notification prompts logs and goal tools explicitly * fix: enforce invariant formatting across #704 prompts and machine text * test: cover invariant decimal formatting for #704 * chore: refresh architecture artifacts for #704 * test: cover remaining culture formatting paths for #704 * Fix last completion date after habit type changes (#574) * fix: preserve habit log slip kind at write time * test: include slip kind in concurrent log fixture * fix: classify old habit logs during rolling deploys * Clean stale text in API repository (#575) * Use neutral names in test fixtures * Make repository text timeless * Remove disallowed narration comment * Add timeless text gate to API repository (#576) * Add timeless text gate to API repository * fix: read comments in XML build files * fix: match script end tags with attributes * fix: end a script only at a real end-tag delimiter * Sync timeless checker with mobile main (#578) * Sync timeless checker with mobile main * fix: let a reviewed directory entry exempt any rule but machine-path * fix: align timeless checker with reviewed version (#580) Add regression coverage for hidden comments and introduced findings. Refs thomasluizon/orbit-tickets#731 * Fix bulk habit replay plans across chat retries (#581) * Fix bulk habit replay when selected chunks change * Preserve bulk habit replay plans across retries * Replay legacy bulk chunks and share replay planning * Scope bulk replay plans by invocation and refuse legacy chat retries * Fix bulk chat replay plan identity by invocation order * Pin FluentValidation default messages to English (#582) * fix: pin FluentValidation default messages to English * chore: regenerate the architecture map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Return a code for habit ceiling failures (#583) * fix: return a code when the habit ceiling is reached * chore: regenerate the architecture map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix Google signup race during session save (#584) * Fix Google signup race tracker cleanup * Pin validator message test culture * chore: generate architecture map outside git (#585) * Cascade bulk habit deletion through subtrees (#587) * fix: cascade bulk habit deletion through subtrees * fix: keep bulk delete results aligned with requested habits * Fix habit checklist reset and general-habit end dates (#586) * Fix habit checklist, date, and reminder invariants * Verify stored reminder JSON fields in migration test * Keep habit checklist and end date fixes * fix: keep database pool connections warm (#590) * Complete error copy for synced habit guards * Refresh gating matrix provenance after copy sync --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Recover concurrent habit log duplicates (#325) (#552) * Start #325 habit log duplicate recovery * Recover duplicate habit logs without losing idempotency response * chore(deps): bump the github-actions group across 1 directory with 5 updates (#530) * chore(deps): bump the github-actions group across 1 directory with 5 updates Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` | | [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` | | [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` | Updates `actions/upload-artifact` from 5 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v5...v7) Updates `github/codeql-action/init` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@db488dd...1c5b675) Updates `actions/setup-java` from 5 to 6 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](actions/setup-java@v5...v6) Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17 - [Release notes](https://github.com/oasdiff/oasdiff-action/releases) - [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/init dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: oasdiff/oasdiff-action/breaking dependency-version: 0.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> * fix: restore optional response removal gate (#664) * fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ORB-101: verify Turnstile tokens on anonymous writes (#550) * chore: open ORB-101 review branch * feat: gate anonymous writes with optional Turnstile verification * test: cover Turnstile gate and verifier failures * test: assert valid Turnstile request returns success * test: ground Turnstile responses in observed Siteverify results Document web, Android, and landing activation checks for ORB-101. * Fix Astra goal access for every plan (#677) (#560) * fix: accept stored recap week anchors after preference changes (#683) (#559) * fix: resolve stored recap week anchors without the current preference A closed week anchor lives in the recap share link and in the stored snapshot key. Validating it against today's WeekStartDay rejected a Monday anchor after the person moved to Sunday, before the stored snapshot could be read. Accept any supported week start (Sunday or Monday) for an explicit closed anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: use closed week anchor for recap cache misses --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Prune idle Hangfire session connections for #564 (#554) * Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558) --- updated-dependencies: - dependency-name: Scalar.AspNetCore dependency-version: 2.17.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Make unsubscribe token tampering test byte-accurate (#673) (#561) * Make goal completion timestamp test deterministic (#676) (#562) * Start #676 goal clock fix * Make goal completion timestamp test deterministic (#676) * chore: regenerate the architecture map for the goal clock test The test now references HabitLog, so node tools/arch-map.mjs adds it to the test's references. Two runs produce the same output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537) * Pin Pullfrog reviewer model and effort for #636 * Set Pullfrog effort to medium on Thomas's decision (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Describe the pinned Pullfrog reviewer in the workflow comment (#636) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort input was dropped. The review step now runs the action's own code at pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a medium rung. Return to @v0 once 0.1.83 is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Take Pullfrog model and effort from the repository config (#565) The app dispatch payload carries model and effort and outranks action inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537 never applied (run 36187465385 resolved effort xhigh). Effort is now 0.25 (medium on GPT-6 Sol) in the Pullfrog repo config. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix relative reminders across local day boundaries (#566) * chore: start reminder boundary fix for #695 * test: inject a fixed clock into relative reminder checks * fix: send relative reminders on their local occurrence day * chore: ratchet the scheduler suppression count and regenerate the map #695 removed the two relative-reminder UTC-date suppressions, so the closed allowlist now declares 2 ORBIT0004 sites in ReminderSchedulerService.cs. The architecture map is regenerated for the new clock dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: fire relative reminders at the first local due time across DST Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is ambiguous and ConvertTimeToUtc picked the later occurrence, an hour after the old scheduler. A repeated hour now resolves to its first occurrence, and a due time inside a spring-forward gap resolves to the moment the clock jumps past it instead of being skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix bulk habit replay idempotency (#567) * chore: open ticket 389 review early * fix: make bulk habit writes replay safe * fix: scope idempotency ledger by command content * fix: scope idempotency ledger by request ordinal * Use published Pullfrog v0 for Codex review (#568) * Start #700 Pullfrog action update * Use published Pullfrog v0 for Codex review * ci: pin both Pullfrog steps to the v0 release commit SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency. Pin the commit the v0 tag named on 2026-09-26, release 0.1.83. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs: align worker PR handoff with ticket orders (#570) * Fix child habit title messages on update for #651 (#572) * Fix explicit culture formatting for #704 (#569) * chore: begin #704 culture formatting work * fix: format #704 notification prompts logs and goal tools explicitly * fix: enforce invariant formatting across #704 prompts and machine text * test: cover invariant decimal formatting for #704 * chore: refresh architecture artifacts for #704 * test: cover remaining culture formatting paths for #704 * Fix last completion date after habit type changes (#574) * fix: preserve habit log slip kind at write time * test: include slip kind in concurrent log fixture * fix: classify old habit logs during rolling deploys * Clean stale text in API repository (#575) * Use neutral names in test fixtures * Make repository text timeless * Remove disallowed narration comment * Add timeless text gate to API repository (#576) * Add timeless text gate to API repository * fix: read comments in XML build files * fix: match script end tags with attributes * fix: end a script only at a real end-tag delimiter * Sync timeless checker with mobile main (#578) * Sync timeless checker with mobile main * fix: let a reviewed directory entry exempt any rule but machine-path * fix: align timeless checker with reviewed version (#580) Add regression coverage for hidden comments and introduced findings. Refs thomasluizon/orbit-tickets#731 * Fix bulk habit replay plans across chat retries (#581) * Fix bulk habit replay when selected chunks change * Preserve bulk habit replay plans across retries * Replay legacy bulk chunks and share replay planning * Scope bulk replay plans by invocation and refuse legacy chat retries * Fix bulk chat replay plan identity by invocation order * Pin FluentValidation default messages to English (#582) * fix: pin FluentValidation default messages to English * chore: regenerate the architecture map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Return a code for habit ceiling failures (#583) * fix: return a code when the habit ceiling is reached * chore: regenerate the architecture map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fix Google signup race during session save (#584) * Fix Google signup race tracker cleanup * Pin validator message test culture * chore: generate architecture map outside git (#585) * Cascade bulk habit deletion through subtrees (#587) * fix: cascade bulk habit deletion through subtrees * fix: keep bulk delete results aligned with requested habits * Fix habit checklist reset and general-habit end dates (#586) * Fix habit checklist, date, and reminder invariants * Verify stored reminder JSON fields in migration test * Keep habit checklist and end date fixes * fix: keep database pool connections warm (#590) * Project scheduler reads to required columns (#589) * Project scheduler reads to notification fields * Deduplicate reminder scheduler projections * Project streak and achievement reads to reduce database egress (#591) * Project distinct streak completion dates in SQL * Project achievement streak and time log fields * Project gamification log inputs for streaks and awards * Bound goal and skip log reads to relevant windows * Project habit schedule fields for streak computations * Update streak repair fixtures for projected reads * fix: preserve backdated standard goal completions * Bound Today schedule log reads (#592) * fix: bound today schedule log reads * test: cover today schedule log read branches * Fix repeated bulk habit calls in keyed requests (#595) * fix: preserve distinct bulk habit call positions (#751) * test: update habit log index expectations (#751) * fix: enforce flexible habit completion target in domain (#751) * test: keep interval month fixture within flexible target (#751) * Preserve relative reminders at local times for due habits (#594) * Fix relative reminders across due times and daylight saving * Align habit tool tests with clock reminder storage * Guard relative reminders without due times and legacy sends * Keep folded clock reminders visible to legacy clients * Fix relative reminder defaults and delivery deduplication * Fix repeat sends for after-due reminders across UTC boundary * Cache reminder scheduler reads between changes (#596) * fix: cache reminder scheduler reads between changes Refresh candidates when habit or user data changes, and bound cached history to one hour. Preserve database uniqueness when scheduler instances overlap. Refs thomasluizon/orbit-tickets#760 * chore: drop the reminder scheduler allowlist entry its suppression no longer needs Refs thomasluizon/orbit-tickets#760 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Reduce completion date query rows for #762 (#598) * test: pin completion date behavior for ticket 762 * fix: project distinct completion dates for freeze and history * fix: narrow achievement streak evidence read * test: measure projected rows for large account * Reduce daily summary log egress for ticket 763 (#599) * test: pin daily summary log behavior for ticket 763 * fix: narrow daily summary log reads to needed dates and values * test: compare narrowed summary prompt with legacy read * test: assert summary log SQL omits unused columns * Add account event stream for live account updates (#601) * Add account event stream with commit-aware delivery * Map event routes and stabilize achievement fixture * Verify reset resync after transaction commit * Verify event ticket claims from a validated access token * fix: publish notification account events * Share habit schedule reads across logging checks (#597) * Share rule eligible habit schedule across log checks * Assert excluded habit logs still count toward achievements * Prove shared schedule SQL on large account seed * Fix habit schedule snapshot invalidation * fix: invalidate habit schedule on transaction retry --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Add child creation timestamps to habit responses (#604) * ORB-194 part 1: remove the ad reward from Astra and deprecate its route (#600) * Remove ad reward claim paths in ORB-194 part 1 * Restore deprecated ad reward endpoint --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep user id out of shared schedule snapshots (#605) * Aggregate reminder scheduler probe (#606) * fix: aggregate reminder scheduler probe * fix: support in memory scheduler tests * fix: preserve reminder probe invalidation across owner swaps * fix: invalidate reminder snapshot on account reset * chore: drop obsolete reminder fold trigger (#608) * fix: keep user id out of shared schedule snapshots (#605) (cherry picked from commit b81d4bf) * Aggregate reminder scheduler probe (#606) * fix: aggregate reminder scheduler probe * fix: support in memory scheduler tests * fix: preserve reminder probe invalidation across owner swaps * fix: invalidate reminder snapshot on account reset (cherry picked from commit 2103aec) * chore: drop obsolete reminder fold trigger (#608) (cherry picked from commit 0a66234) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Support entry point intent (#532) (#523)
* Open support entry intent work
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrpPfdSqgp5twko5kzTvf8
* Add support entry point intent
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrpPfdSqgp5twko5kzTvf8
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Run CI on redesign pull requests (#524)
* chore: initialize #539 pull request
* chore: run CI for redesign pull requests
* fix: compare mutation scope with pull request base
* Add repairable streak gap dates (#518)
* chore: initialize ticket 505
* feat: expose repairable streak gap dates
* chore(arch): regenerate the architecture map for the repairable gap read
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFozk1UXf4Pm5spEHYxzT1
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Sync redesign API branch with main for #670 (#556)
* Add habit widget empty reason (#527)
* Start #372
* Add habit widget empty reason
* ORB-223: Generate gating matrix from PayGateService (#520)
* chore: start ORB-223
* feat: generate gating matrix
* fix: fail closed on unreadable plan gates
* fix: close gating matrix parser gaps
* fix: preserve plans in combined guards
* fix: distinguish quota lifted plans
* fix: carry plan provenance through quota aliases
Derived quota variables were recorded unscoped and only the variable that
literally contains the plan ternary was relabelled afterwards, so plan
provenance did not survive a local alias. A semantics-preserving
`var selectedLimit = user.HasProAccess ? proLimit : freeLimit;
var messageLimit = selectedLimit;` generated cleanly and changed
CanSendAiMessage.quotaLiftedByPlan from "Pro" to null.
The directly plan-selected variables are now labelled first, and the
fixed-point propagation carries the source variable's exact plan instead of
null. An assignment deriving from two different plans cannot have its
provenance proven, so generation fails closed with the same
"cannot derive plan requirement" error the other unprovable shapes use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: fail closed on unknown gating sources
* fix: derive feature flags from migrations
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: fall back to Claude only when the Codex reviewer step fails (#529)
Mirrors thomasluizon/orbit-ui-mobile#1022.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Let the executor own the MCP confirmation gate (#599) (#534)
* fix: let the executor own the MCP confirmation gate (#599)
Every MCP tool whose capability requires a confirmation or a step-up was
refused forever. The selective-auth middleware evaluated the policy a second
time, in front of the executor, and it never received the caller's
confirmation token. A client that stepped up and retried with a valid token
got the identical refusal, because the token never reached that evaluation.
Carrying the token into the middleware cannot fix it. A confirmation token is
single use and is bound to the pending operation's fingerprint, and the
middleware computes a different fingerprint from the raw MCP arguments than
the executor computes from the operation id and its snake_case argument
object. One token cannot satisfy two gates.
So the middleware now steps aside for a confirmation-gated capability, the
same way it already steps aside for execute_agent_operation_v2. Both reach
IAgentOperationExecutor, which evaluates access and confirmation together,
holds the token, and writes the audit row.
Two guard tests pin the invariants the deferral rests on: a confirmation
requirement always sits on a mutation, and every confirmation-gated MCP tool
reaches the executor through McpExecutorBridge.
Also corrected: the step-up message and the three tool parameter descriptions
named verify_step_up_agent_operation_v2 as the source of the confirmation
token. It does not return one. confirm_agent_operation_v2 does.
Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and
nothing ever read it; the real step-up state lives on
PendingAgentOperationState.StepUpSatisfiedAtUtc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: thread the confirmation token through the bulk habit tools (#599)
bulk_log_habits and bulk_skip_habits declared no confirmationToken
parameter and their shared helper hardcoded confirmationToken: null, so
their HabitsBulkWrite capability (FreshConfirmation) could never see a
token and both tools stayed permanently refused. Both now declare the
parameter and ExecuteBulkHabitOperationAsync forwards it.
Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests
pin what the old one missed. The scan now asserts it reaches exactly the
catalog's gated MCP tools, that each tool's forwarded operation id
resolves to a capability with the same ConfirmationRequirement, and that
each tool accepts and forwards a confirmation token. The source scan
walks subfolders and keys members by their declaration rather than by the
first invocation-shaped token in the chunk.
AgentOperationExecutor writes an AgentAuditLogs row before returning
UnknownOperation, restoring the trail the middleware used to leave.
McpConfirmationGateTests now drives the real AgentTools recovery methods
end to end and pins that all three refuse an API-key credential.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599)
The token guard whitelisted one spelling of null. It flagged a bridge call only
when the confirmation-token argument was absent or the literal `null`, so
`confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while
the tool stayed refused forever. A longer literal list does not close that,
because the next spelling is not on the list either.
Assert the structural property instead: a gated tool declares a
`string? confirmationToken` parameter, and the expression it forwards into the
executor's token slot resolves back to that identifier, directly or through one
helper hop. Every other expression fails, whatever it spells.
The declaration check now reads the tool's parameter list rather than its whole
body, so a local named `confirmationToken` no longer satisfies it.
Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It
compared only the confirmation requirement, so a gated tool could forward
another gated capability's operation id, keep confirmation firing, and have the
executor enforce the wrong scope.
The file's doc comment said four invariants and listed four; the file holds
five. Name the source-scan invariant the other three rest on.
Six mutations, each red on its own, real tree green at every step:
`confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool
forwarding a different local, and `delete_tag` forwarding `delete_goal`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: strip only a real named-argument prefix in the MCP guard parser (#599)
ArgumentAt treated any colon as a named-argument separator, so a conditional
expression in the token slot or the operation-id slot collapsed to its
else-branch. `ids.Count > 0 ? null : confirmationToken` read as
`confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read
as `"delete_tag"`, and both guards stayed green over the restored defect.
Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and
every other expression reaches the resolver whole. The anchor keeps a colon
inside a string literal and a `::` qualifier from matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: separate an omitted caller argument from a non-parameter expression (#599)
ResolveExpression returned the expression itself both when it was not a callee
parameter and when the caller supplied nothing at that index. The second case
handed back the helper's own parameter name, which then compared equal to
`confirmationToken` and passed. Giving the helper an optional
`string? confirmationToken = null` and calling it without that argument restored
the #599 defect with the guard green.
Return the `<omitted>` sentinel for the second case instead. It can never equal
the parameter name, and it resolves to no capability in the operation-id slot,
so both guards fail closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: read a gated tool's direct and helper bridge calls together (#599)
CollectBridgeCalls returned early as soon as the tool body held one bridge
call, so a correct call in a dead branch hid every helper call in the same
tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken`
plus a same-file helper forwarding `null` restored the #599 defect with the
guard green.
Collect both sets instead. Every bridge call a gated tool can reach, directly
or through one helper hop, now has to forward the parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: refuse a gated tool that assigns to its own confirmationToken (#599)
The rule compared the forwarded expression's text only, so
`confirmationToken = null;` above the bridge call restored the #599 defect with
the forwarded identifier unchanged and the guard green.
Add an offender when the tool body, or a same-file helper it reaches, assigns
to the parameter. MemberSource now carries its statements with the parameter
list excluded, so the declaration's own `string? confirmationToken = null`
default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`,
`!=`, `>=`, `<=`, `??=` and a lambda arrow alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say what the MCP token resolver models, not more (#599)
Invariant 5 claimed "any other expression in the token slot refuses the tool
forever" and that the tool forwards the identifier "directly or through a
helper it calls". A conditional refuses the tool on some paths only, and the
resolver models exactly one same-file helper hop matched by argument position.
Replace both sentences with what the scan actually does, and state its limits:
two or more hops read as no bridge call and fail the routing guard, and
aliasing, reflection and an interface call are outside the model.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: catch a compound assignment to confirmationToken (#599)
The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??`
of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the
bridge call compiled green with all five guards green, and a later change
that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would
hand the executor a token the MCP caller never sent: HasFreshConfirmation is
then satisfied from state the caller does not control, while the middleware
has already stepped aside.
Admit the two compound operators a `string?` can carry. `==` still fails on
the second `=`, `!=`, `>=` and `<=` still fail because those characters break
`\s*` and are not in the alternation, `=>` still fails on the trailing
character class, and a bare `??` with no `=` still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: read every same-named helper an MCP tool could be calling (#599)
The member index was a dictionary keyed on the bare name and built with
group.First(), so two same-named members collapsed to whichever was declared
first. The scan then analysed a method the tool never calls. It broke both
ways: a decoy overload declared after the real helper and forwarding null was
green, and a gated tool calling a correct overload reddened when an unrelated
same-named member happened to be declared first.
Hold every member under its name and, at each call site, read every candidate
whose parameter count can admit that many arguments. One candidate resolves
the call; more than one is ambiguous, so all of them are read and any bad one
reddens the tool. That fails closed instead of guessing, and it costs no false
red in the case above, where only the real helper carries a bridge call. A
name that admits no candidate stays unresolved, which hides nothing: the
bridge call inside it is not collected either, so the routing guard reddens
the tool.
Arity, not the parameter type, is what decides a candidate. The scan is a text
scan, so it cannot type-check an argument.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say what invariant 5 now refuses and what it still cannot see (#599)
The general claim "refuses a tool that assigns to the parameter" is now exact:
`=`, `??=` and `+=`. The helper hop no longer claims a "matching argument
position", because the resolver pairs by position alone and strips a
named-argument prefix without reading the name, so arguments named out of the
declared order are read wrong. The comment said nothing about overloads, so
say that an ambiguous name is read as every candidate and reddens the tool.
Close with the honest limit: a text scan is defeatable by an author who sets
out to defeat it, and what the guard closes is every shape an ordinary
refactor produces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Fix calendar event timezone projection (#521)
* Start calendar timezone fix
* Fix calendar event timezone projection
* Fix projected calendar end times
* Fix calendar end time during DST fallback
* Fix calendar end time minute precision
* Fix projected calendar recurrence rule
* Guard calendar recurrence across timezone shifts
* Project calendar recurrence weekdays
* Keep calendar recurrence unchanged
* Omit unrepresentable recurring calendar events
* Gate projected calendar recurrence on the whole fetch window
Closes the six review findings on pull request 521.
1. The weekday gate now walks every expanded occurrence Google returned for a
recurring master, not the one sampled instance. The fetcher collects each
instance start with the source calendar's own offset into a JsonIgnore'd
ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in
January and shifts to Wednesday in July is refused. The empty list still
falls back to the sampled instance for an all-day series and for a
suggestion row read back from the database.
2. The legacy title plus date plus time key only excludes a suggestion when
exactly one candidate carries it. Inside a fall-back repeated hour two
events project to the same local time, so the key proves nothing and both
stay. This mirrors the group.Count() == 1 guard the auto sync reconciler
already applies to the same key.
3. An omitted end time now logs its reason at Debug with the event id and the
user id. EndUtc already ships the real duration, so the client keeps it.
4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are
reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule
it imports and cannot re-express without inventing a weekday.
5. Both new call sites pass the logger and the user id to FindTimeZone, which
also catches InvalidTimeZoneException so a corrupt zone stops escaping as a
500. User.SetTimeZone trims at the boundary and rejects a blank id.
6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the
sub hour offset gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Prove calendar recurrence stability from both zones
Round 6 gated a BYDAY series on the expanded instances Google returned, and
GoogleCalendarApi only asks for sixty days, so a series whose transition falls
outside that window was admitted and a stored suggestion row never carried the
instances at all.
The gate now reads the source calendar's own timezone from the recurring master
the fetcher already fetches, and walks a year of dates at the occurrence's source
wall clock through both zones' rules. A series it cannot prove stable is withheld
from both feeds. StoredCalendarEventJson carries the source zone beside the stored
suggestion, so the suggestion feed judges a row on the same evidence the events
feed had.
Also in scope: the auto-sync reconciler projects a fetched event into the account
timezone before matching a legacy habit, the end-time omission logs whatever EndUtc
holds, and an all-day event no longer reports Google's exclusive end date as an
end instant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep calendar series a spring forward gap cannot move
A probe date whose source wall clock a spring forward gap removes is a date
the series does not fire on, not evidence the series moves. The walk ended
there with "unproved" and the gate withheld the whole series, so an account
reading a calendar kept in its own zone lost an hour wide band in thirteen
DST zones even though the projection is the identity.
HasUnrepresentableRecurrenceAfterProjection now answers false as soon as
TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone
removes instead of ending. RFC 5545 section 3.3.5 does define the missing
case, but probing that normalized instant changes no decision for any of the
14,752 zone pairs that could distinguish it, and asserting an expansion
Google does not document would let one guessed date withhold a year.
Also pins what earlier rounds argued: auto-sync refusing to store or notify a
withheld series, JsonIgnore keeping SourceTimeZone off the response body, the
repeated hour needing both of its instants, the probe reaching a full year,
and an all day event logging no dropped end time. A stored SourceTimeZone key
holding a number now reads as no source zone rather than failing the request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix recurring calendar timezone proof and legacy refresh
* Avoid refreshing legacy calendar rows with duplicate event IDs
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Bump FluentAssertions and 23 others (#538)
Bumps FluentAssertions from 8.10.0 to 8.11.0
Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277
Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25
Bumps Hangfire.Core from 1.8.24 to 1.8.25
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12
Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12
Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0
Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1
Bumps OpenAI from 2.13.0 to 2.14.0
Bumps PostHog from 2.14.0 to 2.15.7
Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7
Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9
Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1
Bumps Stripe.net from 52.3.0 to 52.4.2
---
updated-dependencies:
- dependency-name: Google.Apis.AndroidPublisher.v3
dependency-version: 1.76.0.4277
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.AspNetCore
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.OpenApi
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Design
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Relational
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.ApiDescription.Server
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Abstractions
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Http
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
dependency-version: 8.23.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: OpenAI
dependency-version: 2.14.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: PostHog
dependency-version: 2.15.7
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: PostHog.AspNetCore
dependency-version: 2.9.7
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Scalar.AspNetCore
dependency-version: 2.17.9
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Sentry.AspNetCore
dependency-version: 6.11.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Stripe.net
dependency-version: 52.4.2
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Memory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Give API key management one authorization concept with two doors (#528)
* Start #529 API key step up
* Require API key management step up
* chore: regenerate the architecture map
The step up change moved the API key endpoints' shape, so `architecture.json`
and `architecture.html` no longer matched the tree and the `drift` check
failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`,
which reports 45 entities and 0 untested feature folders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give API key management one authorization concept with two doors
Listing, creating and revoking API keys now read one grant. Two doors open
it, and both end in a six-digit code emailed to the account owner: the HTTP
challenge, and a verified agent step-up.
Creating key material spends the grant. Listing and revoking only read it,
so one emailed code authorizes a management session instead of exactly one
revoke. A revoke no longer locks the person out of the key list.
get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read
capability to a step-up inside the executor, so the read opens a pending
operation the caller can step up against, and the MCP read routes through
McpExecutorBridge like the mutation does.
AppConfigService now parses a stored row strictly and throws by name when a
row is malformed, so a typo cannot leave the gate off while the read-back
reports it on.
Both controller actions declare 403 and 428, and openapi.json carries them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Cover API key step up through merged MCP gate
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Expose habit emoji in MCP tools (#513) (#546)
* Open ticket 513 implementation review
* Expose habit emoji across MCP tools (#513)
* Prevent stale pull request workflow runs for #659 (#555)
* Start #659 workflow concurrency change
* Group core PR workflows by pull request for #659
* Group remaining PR workflows by pull request for #659
* Fix #657 logout revocation across a session family (#553)
* Start #657 session family fix
* Revoke the full auth session after logout (#657)
* Explain temporary legacy token allowance (#657)
* Handle duplicate refresh history race (#657)
* Fix #588 bulk habit interval weeks (#539)
* Start #588 bulk interval weeks fix
* Fix #588 bulk habit interval weeks mapping
* Guarantee crisis resources in Astra chat for #319 (#547)
* Add crisis guidance to Astra static prompt for #319
* Add curated crisis detection and regression cases for #319
* Wire crisis guard across chat delivery and metrics for #319
* Refine crisis delivery and cover fallback regression for #319
* Return static crisis support when AI chat fails for #319
* Keep crisis FAQ regression isolated for #319
* Fix crisis replies to bypass AI quota and provider
* Record streak freeze source for #571 (#540)
* Start #571 freeze source work
* Record streak freeze origin for #571
* Require explicit origin for new streak freezes
* Keep support tool available for support entry point (#541)
* Expose scheduled streak repair gap dates for #505 (#542)
* Fix standalone sub habit title validation for #628 (#543)
* Start #628 sub habit title validation fix
* Use sub habit title messages in standalone validation (#628)
* Carry the onboarding repeat interval into the created habit (#596) (#533)
A signed-out person who set a weekly repeat interval during onboarding lost
it at sign-up. ApplyHabitInput carried no interval field at all, so the apply
path dropped the value between the screen and the record.
Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it
to Habit.Create alongside the other schedule options. No existing field
changes name, shape or nullability, so a client that sends nothing keeps the
behaviour it has today: IntervalWeeks stays null and the habit repeats every
week.
Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules
range, the same 1 to 52 bound every other create path uses.
Regenerate openapi.json and architecture.json/html for the new field and for
the test class that now touches HabitScheduleService.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Expose recurrence time zone in calendar events for #591 (#544)
* Start #591 recurrence time zone work
* Expose recurring calendar event time zone for #591
* Restore recurrence time zone in legacy suggestions for #591
* Project safe calendar BYDAY recurrences (#569) (#545)
* Start calendar BYDAY projection (#569)
* Project uniform calendar BYDAY recurrences (#569)
* Cover alternate week recurrence projection (#569)
* Keep shifted calendar rules importable by installed clients
* Add closed week and year recap ranges for #369 (#548)
* Fix Google sign-in after concurrent user updates (#551)
* chore: start ticket 324 review
* fix: retry concurrent Google sign-in updates (#324)
* Fix duplicate agent confirmation consumption (#557)
* Start fix for ticket 671 confirmation race
* Claim agent confirmation once under concurrent saves
* chore: regenerate the architecture map for the confirmation claim migration (#671)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: show newest habit completion date in profile (#549)
* chore: start ticket 391
* fix: expose latest habit completion date in profile
* fix: exclude bad habit slips from last completion date
* fix: pass the freeze origin in the profile freeze test after #540 (#391)
The merge with main brought StreakFreeze.Create's required origin
parameter (#540). The test now asserts that a freeze without a
completion leaves lastCompletionDate null for both origins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: preserve last completion across habit cleanup (#391)
* Preserve descendant completions during sync cleanup
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: resolve stored recap week anchors without the current preference
A closed week anchor lives in the recap share link and in the stored
snapshot key. Validating it against today's WeekStartDay rejected a
Monday anchor after the person moved to Sunday, before the stored
snapshot could be read. Accept any supported week start (Sunday or
Monday) for an explicit closed anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Redesign the transactional emails, AI push copy and error messages (R20) (#532)
* Add habit widget empty reason (#527)
* Start #372
* Add habit widget empty reason
* ORB-223: Generate gating matrix from PayGateService (#520)
* chore: start ORB-223
* feat: generate gating matrix
* fix: fail closed on unreadable plan gates
* fix: close gating matrix parser gaps
* fix: preserve plans in combined guards
* fix: distinguish quota lifted plans
* fix: carry plan provenance through quota aliases
Derived quota variables were recorded unscoped and only the variable that
literally contains the plan ternary was relabelled afterwards, so plan
provenance did not survive a local alias. A semantics-preserving
`var selectedLimit = user.HasProAccess ? proLimit : freeLimit;
var messageLimit = selectedLimit;` generated cleanly and changed
CanSendAiMessage.quotaLiftedByPlan from "Pro" to null.
The directly plan-selected variables are now labelled first, and the
fixed-point propagation carries the source variable's exact plan instead of
null. An assignment deriving from two different plans cannot have its
provenance proven, so generation fails closed with the same
"cannot derive plan requirement" error the other unprovable shapes use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: fail closed on unknown gating sources
* fix: derive feature flags from migrations
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: fall back to Claude only when the Codex reviewer step fails (#529)
Mirrors thomasluizon/orbit-ui-mobile#1022.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Redesign the transactional emails, AI push copy and error messages
Closes thomasluizon/orbit-tickets#75 (R20).
Emails. All 7 types plus the shared wrapper move to the granted canon: the
violet #7F46F7 becomes the warm orange #C4530F, the gradient header band and
every CTA glow are gone (DESIGN.md bans both), Rubik and Inter become the
Geist Sans and Geist Mono stacks, and the planet emoji and sparkle bullets
give way to the mark and a plain numbered list. Emails now author light and
override dark through prefers-color-scheme, which matches "light mode
MANDATORY, dark primary" and survives a forced inversion far better than the
dark only document did. Copy is rewritten in both languages to say what
happened before what to do next. Support copy moves out of its template into
EmailCopy so markup holds no strings.
AI push copy. The slip alert, proactive check in and daily summary prompts
demonstrated an exclamation mark and a doubled hyphen inside their own worked
examples, and at temperature 0.9 the model reproduced both. One shared voice
contract, NotificationVoice, now states each ban and carries no example at
all. The fallback strings carried the same two tells and are rewritten.
Error messages. A domain guard firing is an ordinary event, but its message
is the sentence a developer wrote, and on 2026-08-05 a tester read "Days can
only be set when frequency quantity is 1." verbatim, in English, inside a
pt-BR interface. ErrorCopy now maps every error code to user facing copy in
both languages, and one result filter swaps the message at the response
boundary. DomainErrors stays developer facing, as the ticket requires. The
catalog is total over ErrorCodes, ErrorMessages and DomainErrors, and a
reflection test fails the build when a constant arrives without copy, so the
raw message fallback is unreachable. ErrorMessages now takes its English from
the same catalog rather than repeating 145 sentences in a second file.
AppError and Result carry the arguments behind a {0} placeholder so the
localized template can be formatted with the same values, which the eagerly
formatted message alone no longer exposed.
Also removed: five pay gate strings ending in "Upgrade to unlock!", a banned
word and an exclamation on a sell; inlined copies of error sentences in three
chat tools; and a hardcoded 500 envelope message.
Tests: 6980 passing, 0 failing. Line coverage 86.40%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Build the banned dashes from code points and refresh the architecture map
Two red checks on PR 532 at c50f052e.
Dash Ban. The three new voice tests detected a banned dash by embedding one,
and the gate scans a test file like any other. The two constants are now built
from their code points, ((char)0x2014) and ((char)0x2013), so no literal
character sits in the source. The escape form these files were written with
did not survive: the lefthook pre-commit step runs dotnet format, which
normalized "—" back into the raw character before the commit landed. A
code point arithmetic expression has no literal for a formatter to normalize.
The assertions are unchanged. Verified by injecting a real em dash into one
ErrorCopy entry and one EmailCopy string: EveryEntryObeysTheErrorVoice and
NoEmailStringCarriesADashCharacter both went red, then green again once the
injection was reverted.
drift. architecture.json and architecture.html were stale against the new
test classes and the changed file counts. Regenerated with node
tools/arch-map.mjs and committed here, in the change that requires them. The
generator is deterministic: two consecutive runs are byte identical.
tools/dash-baseline.json is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix the four blocking findings on the email and error copy pass
C1: the deletion email promised a permanent wipe while the product only
deactivates. The intro now states the grace window, read from
AppConstants.MaxDeletionGraceDays, and that signing in again cancels it.
C2: the copy pass had moved the wrong-code failure onto a new error code
both shipped clients have no branch for, which sends Orbit 1.3.31 and the
web step-up screen to a generic error. The code returns to
INVALID_VERIFICATION_CODE, and the count rides in a counted copy variant
that keeps the trailing token those clients parse.
C3: the reply language came from Accept-Language alone, which neither the
Android app nor the web Server Actions send, so every signed-in pt-BR
reader got English. RequestLanguageResolver reads the stored
User.Language first and keeps the header as the anonymous fallback. The
header parse now honours quality values.
C4: the slip alert claimed the usual time had arrived, while the
scheduler sends two hours early and at 08:00 when no peak hour exists.
The prompt states when the push lands and the fallback makes no time
claim without a peak hour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Make the copy claims match the code that produces them
C5: the check-in fallback called one open habit "a few" and promised that
Astra records the rest. The scheduler sends as soon as one habit is off
track, and LogHabitTool records only the habit a person names.
C6: the 429 body was an anonymous object the localizing filter could not
match, so the RATE_LIMITED copy was unreachable. It is built from
ErrorMessages.TooManyRequests now, which also gives the 429 an error code
for the first time. Retry-After and the request id already ride in
headers.
C8: the sync copy promised a reload and a later send that no caller
performs. C9: a challenge does not take part, people do. C10: the recap
arrives only for a reader with a push subscription, so the copy points at
coming back instead.
D1 and D4: two doc comments claimed more than the code does. The filter
covers ErrorResponse bodies only, and a FluentValidation message never
reaches it. A missing catalog entry throws on the first request, not at
startup.
D3: BuildSummaryPrompt is internal and joins both voice theory sources,
so the guard written for its doubled-hyphen leak now reaches it.
D5: AppError compared Args by reference, so two errors formatted with the
same count came out unequal. Equality walks the arguments now.
The three voice tests that read NotificationVoice.Rules' own literals
back out of it now assert the bans over every prompt a model receives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Refresh the architecture map for the review round
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let the executor own the MCP confirmation gate (#599) (#534)
* fix: let the executor own the MCP confirmation gate (#599)
Every MCP tool whose capability requires a confirmation or a step-up was
refused forever. The selective-auth middleware evaluated the policy a second
time, in front of the executor, and it never received the caller's
confirmation token. A client that stepped up and retried with a valid token
got the identical refusal, because the token never reached that evaluation.
Carrying the token into the middleware cannot fix it. A confirmation token is
single use and is bound to the pending operation's fingerprint, and the
middleware computes a different fingerprint from the raw MCP arguments than
the executor computes from the operation id and its snake_case argument
object. One token cannot satisfy two gates.
So the middleware now steps aside for a confirmation-gated capability, the
same way it already steps aside for execute_agent_operation_v2. Both reach
IAgentOperationExecutor, which evaluates access and confirmation together,
holds the token, and writes the audit row.
Two guard tests pin the invariants the deferral rests on: a confirmation
requirement always sits on a mutation, and every confirmation-gated MCP tool
reaches the executor through McpExecutorBridge.
Also corrected: the step-up message and the three tool parameter descriptions
named verify_step_up_agent_operation_v2 as the source of the confirmation
token. It does not return one. confirm_agent_operation_v2 does.
Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and
nothing ever read it; the real step-up state lives on
PendingAgentOperationState.StepUpSatisfiedAtUtc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: thread the confirmation token through the bulk habit tools (#599)
bulk_log_habits and bulk_skip_habits declared no confirmationToken
parameter and their shared helper hardcoded confirmationToken: null, so
their HabitsBulkWrite capability (FreshConfirmation) could never see a
token and both tools stayed permanently refused. Both now declare the
parameter and ExecuteBulkHabitOperationAsync forwards it.
Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests
pin what the old one missed. The scan now asserts it reaches exactly the
catalog's gated MCP tools, that each tool's forwarded operation id
resolves to a capability with the same ConfirmationRequirement, and that
each tool accepts and forwards a confirmation token. The source scan
walks subfolders and keys members by their declaration rather than by the
first invocation-shaped token in the chunk.
AgentOperationExecutor writes an AgentAuditLogs row before returning
UnknownOperation, restoring the trail the middleware used to leave.
McpConfirmationGateTests now drives the real AgentTools recovery methods
end to end and pins that all three refuse an API-key credential.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599)
The token guard whitelisted one spelling of null. It flagged a bridge call only
when the confirmation-token argument was absent or the literal `null`, so
`confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while
the tool stayed refused forever. A longer literal list does not close that,
because the next spelling is not on the list either.
Assert the structural property instead: a gated tool declares a
`string? confirmationToken` parameter, and the expression it forwards into the
executor's token slot resolves back to that identifier, directly or through one
helper hop. Every other expression fails, whatever it spells.
The declaration check now reads the tool's parameter list rather than its whole
body, so a local named `confirmationToken` no longer satisfies it.
Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It
compared only the confirmation requirement, so a gated tool could forward
another gated capability's operation id, keep confirmation firing, and have the
executor enforce the wrong scope.
The file's doc comment said four invariants and listed four; the file holds
five. Name the source-scan invariant the other three rest on.
Six mutations, each red on its own, real tree green at every step:
`confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool
forwarding a different local, and `delete_tag` forwarding `delete_goal`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: strip only a real named-argument prefix in the MCP guard parser (#599)
ArgumentAt treated any colon as a named-argument separator, so a conditional
expression in the token slot or the operation-id slot collapsed to its
else-branch. `ids.Count > 0 ? null : confirmationToken` read as
`confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read
as `"delete_tag"`, and both guards stayed green over the restored defect.
Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and
every other expression reaches the resolver whole. The anchor keeps a colon
inside a string literal and a `::` qualifier from matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: separate an omitted caller argument from a non-parameter expression (#599)
ResolveExpression returned the expression itself both when it was not a callee
parameter and when the caller supplied nothing at that index. The second case
handed back the helper's own parameter name, which then compared equal to
`confirmationToken` and passed. Giving the helper an optional
`string? confirmationToken = null` and calling it without that argument restored
the #599 defect with the guard green.
Return the `<omitted>` sentinel for the second case instead. It can never equal
the parameter name, and it resolves to no capability in the operation-id slot,
so both guards fail closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: read a gated tool's direct and helper bridge calls together (#599)
CollectBridgeCalls returned early as soon as the tool body held one bridge
call, so a correct call in a dead branch hid every helper call in the same
tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken`
plus a same-file helper forwarding `null` restored the #599 defect with the
guard green.
Collect both sets instead. Every bridge call a gated tool can reach, directly
or through one helper hop, now has to forward the parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: refuse a gated tool that assigns to its own confirmationToken (#599)
The rule compared the forwarded expression's text only, so
`confirmationToken = null;` above the bridge call restored the #599 defect with
the forwarded identifier unchanged and the guard green.
Add an offender when the tool body, or a same-file helper it reaches, assigns
to the parameter. MemberSource now carries its statements with the parameter
list excluded, so the declaration's own `string? confirmationToken = null`
default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`,
`!=`, `>=`, `<=`, `??=` and a lambda arrow alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say what the MCP token resolver models, not more (#599)
Invariant 5 claimed "any other expression in the token slot refuses the tool
forever" and that the tool forwards the identifier "directly or through a
helper it calls". A conditional refuses the tool on some paths only, and the
resolver models exactly one same-file helper hop matched by argument position.
Replace both sentences with what the scan actually does, and state its limits:
two or more hops read as no bridge call and fail the routing guard, and
aliasing, reflection and an interface call are outside the model.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: catch a compound assignment to confirmationToken (#599)
The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??`
of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the
bridge call compiled green with all five guards green, and a later change
that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would
hand the executor a token the MCP caller never sent: HasFreshConfirmation is
then satisfied from state the caller does not control, while the middleware
has already stepped aside.
Admit the two compound operators a `string?` can carry. `==` still fails on
the second `=`, `!=`, `>=` and `<=` still fail because those characters break
`\s*` and are not in the alternation, `=>` still fails on the trailing
character class, and a bare `??` with no `=` still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: read every same-named helper an MCP tool could be calling (#599)
The member index was a dictionary keyed on the bare name and built with
group.First(), so two same-named members collapsed to whichever was declared
first. The scan then analysed a method the tool never calls. It broke both
ways: a decoy overload declared after the real helper and forwarding null was
green, and a gated tool calling a correct overload reddened when an unrelated
same-named member happened to be declared first.
Hold every member under its name and, at each call site, read every candidate
whose parameter count can admit that many arguments. One candidate resolves
the call; more than one is ambiguous, so all of them are read and any bad one
reddens the tool. That fails closed instead of guessing, and it costs no false
red in the case above, where only the real helper carries a bridge call. A
name that admits no candidate stays unresolved, which hides nothing: the
bridge call inside it is not collected either, so the routing guard reddens
the tool.
Arity, not the parameter type, is what decides a candidate. The scan is a text
scan, so it cannot type-check an argument.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say what invariant 5 now refuses and what it still cannot see (#599)
The general claim "refuses a tool that assigns to the parameter" is now exact:
`=`, `??=` and `+=`. The helper hop no longer claims a "matching argument
position", because the resolver pairs by position alone and strips a
named-argument prefix without reading the name, so arguments named out of the
declared order are read wrong. The comment said nothing about overloads, so
say that an ambiguous name is read as every candidate and reddens the tool.
Close with the honest limit: a text scan is defeatable by an author who sets
out to defeat it, and what the guard closes is every shape an ordinary
refactor produces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Fix calendar event timezone projection (#521)
* Start calendar timezone fix
* Fix calendar event timezone projection
* Fix projected calendar end times
* Fix calendar end time during DST fallback
* Fix calendar end time minute precision
* Fix projected calendar recurrence rule
* Guard calendar recurrence across timezone shifts
* Project calendar recurrence weekdays
* Keep calendar recurrence unchanged
* Omit unrepresentable recurring calendar events
* Gate projected calendar recurrence on the whole fetch window
Closes the six review findings on pull request 521.
1. The weekday gate now walks every expanded occurrence Google returned for a
recurring master, not the one sampled instance. The fetcher collects each
instance start with the source calendar's own offset into a JsonIgnore'd
ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in
January and shifts to Wednesday in July is refused. The empty list still
falls back to the sampled instance for an all-day series and for a
suggestion row read back from the database.
2. The legacy title plus date plus time key only excludes a suggestion when
exactly one candidate carries it. Inside a fall-back repeated hour two
events project to the same local time, so the key proves nothing and both
stay. This mirrors the group.Count() == 1 guard the auto sync reconciler
already applies to the same key.
3. An omitted end time now logs its reason at Debug with the event id and the
user id. EndUtc already ships the real duration, so the client keeps it.
4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are
reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule
it imports and cannot re-express without inventing a weekday.
5. Both new call sites pass the logger and the user id to FindTimeZone, which
also catches InvalidTimeZoneException so a corrupt zone stops escaping as a
500. User.SetTimeZone trims at the boundary and rejects a blank id.
6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the
sub hour offset gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Prove calendar recurrence stability from both zones
Round 6 gated a BYDAY series on the expanded instances Google returned, and
GoogleCalendarApi only asks for sixty days, so a series whose transition falls
outside that window was admitted and a stored suggestion row never carried the
instances at all.
The gate now reads the source calendar's own timezone from the recurring master
the fetcher already fetches, and walks a year of dates at the occurrence's source
wall clock through both zones' rules. A series it cannot prove stable is withheld
from both feeds. StoredCalendarEventJson carries the source zone beside the stored
suggestion, so the suggestion feed judges a row on the same evidence the events
feed had.
Also in scope: the auto-sync reconciler projects a fetched event into the account
timezone before matching a legacy habit, the end-time omission logs whatever EndUtc
holds, and an all-day event no longer reports Google's exclusive end date as an
end instant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep calendar series a spring forward gap cannot move
A probe date whose source wall clock a spring forward gap removes is a date
the series does not fire on, not evidence the series moves. The walk ended
there with "unproved" and the gate withheld the whole series, so an account
reading a calendar kept in its own zone lost an hour wide band in thirteen
DST zones even though the projection is the identity.
HasUnrepresentableRecurrenceAfterProjection now answers false as soon as
TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone
removes instead of ending. RFC 5545 section 3.3.5 does define the missing
case, but probing that normalized instant changes no decision for any of the
14,752 zone pairs that could distinguish it, and asserting an expansion
Google does not document would let one guessed date withhold a year.
Also pins what earlier rounds argued: auto-sync refusing to store or notify a
withheld series, JsonIgnore keeping SourceTimeZone off the response body, the
repeated hour needing both of its instants, the probe reaching a full year,
and an all day event logging no dropped end time. A stored SourceTimeZone key
holding a number now reads as no source zone rather than failing the request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix recurring calendar timezone proof and legacy refresh
* Avoid refreshing legacy calendar rows with duplicate event IDs
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Bump FluentAssertions and 23 others (#538)
Bumps FluentAssertions from 8.10.0 to 8.11.0
Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277
Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25
Bumps Hangfire.Core from 1.8.24 to 1.8.25
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12
Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12
Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0
Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1
Bumps OpenAI from 2.13.0 to 2.14.0
Bumps PostHog from 2.14.0 to 2.15.7
Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7
Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9
Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1
Bumps Stripe.net from 52.3.0 to 52.4.2
---
updated-dependencies:
- dependency-name: Google.Apis.AndroidPublisher.v3
dependency-version: 1.76.0.4277
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.AspNetCore
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
dependency-version: 1.8.25
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.OpenApi
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Design
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Relational
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.ApiDescription.Server
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Abstractions
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Http
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
dependency-version: 8.23.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: OpenAI
dependency-version: 2.14.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: PostHog
dependency-version: 2.15.7
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: PostHog.AspNetCore
dependency-version: 2.9.7
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Scalar.AspNetCore
dependency-version: 2.17.9
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Sentry.AspNetCore
dependency-version: 6.11.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Stripe.net
dependency-version: 52.4.2
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Memory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
dependency-version: 8.11.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
dependency-version: 18.10.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nuget-minor-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Give API key management one authorization concept with two doors (#528)
* Start #529 API key step up
* Require API key management step up
* chore: regenerate the architecture map
The step up change moved the API key endpoints' shape, so `architecture.json`
and `architecture.html` no longer matched the tree and the `drift` check
failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`,
which reports 45 entities and 0 untested feature folders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give API key management one authorization concept with two doors
Listing, creating and revoking API keys now read one grant. Two doors open
it, and both end in a six-digit code emailed to the account owner: the HTTP
challenge, and a verified agent step-up.
Creating key material spends the grant. Listing and revoking only read it,
so one emailed code authorizes a management session instead of exactly one
revoke. A revoke no longer locks the person out of the key list.
get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read
capability to a step-up inside the executor, so the read opens a pending
operation the caller can step up against, and the MCP read routes through
McpExecutorBridge like the mutation does.
AppConfigService now parses a stored row strictly and throws by name when a
row is malformed, so a typo cannot leave the gate off while the read-back
reports it on.
Both controller actions declare 403 and …
) * Support entry point intent (#532) (#523) * Open support entry intent work Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VrpPfdSqgp5twko5kzTvf8 * Add support entry point intent Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VrpPfdSqgp5twko5kzTvf8 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Run CI on redesign pull requests (#524) * chore: initialize #539 pull request * chore: run CI for redesign pull requests * fix: compare mutation scope with pull request base * Add repairable streak gap dates (#518) * chore: initialize ticket 505 * feat: expose repairable streak gap dates * chore(arch): regenerate the architecture map for the repairable gap read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFozk1UXf4Pm5spEHYxzT1 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Sync redesign API branch with main for #670 (#556) * Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller actions declare 403 and 428, and openapi.json carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Cover API key step up through merged MCP gate --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Expose habit emoji in MCP tools (#513) (#546) * Open ticket 513 implementation review * Expose habit emoji across MCP tools (#513) * Prevent stale pull request workflow runs for #659 (#555) * Start #659 workflow concurrency change * Group core PR workflows by pull request for #659 * Group remaining PR workflows by pull request for #659 * Fix #657 logout revocation across a session family (#553) * Start #657 session family fix * Revoke the full auth session after logout (#657) * Explain temporary legacy token allowance (#657) * Handle duplicate refresh history race (#657) * Fix #588 bulk habit interval weeks (#539) * Start #588 bulk interval weeks fix * Fix #588 bulk habit interval weeks mapping * Guarantee crisis resources in Astra chat for #319 (#547) * Add crisis guidance to Astra static prompt for #319 * Add curated crisis detection and regression cases for #319 * Wire crisis guard across chat delivery and metrics for #319 * Refine crisis delivery and cover fallback regression for #319 * Return static crisis support when AI chat fails for #319 * Keep crisis FAQ regression isolated for #319 * Fix crisis replies to bypass AI quota and provider * Record streak freeze source for #571 (#540) * Start #571 freeze source work * Record streak freeze origin for #571 * Require explicit origin for new streak freezes * Keep support tool available for support entry point (#541) * Expose scheduled streak repair gap dates for #505 (#542) * Fix standalone sub habit title validation for #628 (#543) * Start #628 sub habit title validation fix * Use sub habit title messages in standalone validation (#628) * Carry the onboarding repeat interval into the created habit (#596) (#533) A signed-out person who set a weekly repeat interval during onboarding lost it at sign-up. ApplyHabitInput carried no interval field at all, so the apply path dropped the value between the screen and the record. Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it to Habit.Create alongside the other schedule options. No existing field changes name, shape or nullability, so a client that sends nothing keeps the behaviour it has today: IntervalWeeks stays null and the habit repeats every week. Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules range, the same 1 to 52 bound every other create path uses. Regenerate openapi.json and architecture.json/html for the new field and for the test class that now touches HabitScheduleService. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Expose recurrence time zone in calendar events for #591 (#544) * Start #591 recurrence time zone work * Expose recurring calendar event time zone for #591 * Restore recurrence time zone in legacy suggestions for #591 * Project safe calendar BYDAY recurrences (#569) (#545) * Start calendar BYDAY projection (#569) * Project uniform calendar BYDAY recurrences (#569) * Cover alternate week recurrence projection (#569) * Keep shifted calendar rules importable by installed clients * Add closed week and year recap ranges for #369 (#548) * Fix Google sign-in after concurrent user updates (#551) * chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324) * Fix duplicate agent confirmation consumption (#557) * Start fix for ticket 671 confirmation race * Claim agent confirmation once under concurrent saves * chore: regenerate the architecture map for the confirmation claim migration (#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: show newest habit completion date in profile (#549) * chore: start ticket 391 * fix: expose latest habit completion date in profile * fix: exclude bad habit slips from last completion date * fix: pass the freeze origin in the profile freeze test after #540 (#391) The merge with main brought StreakFreeze.Create's required origin parameter (#540). The test now asserts that a freeze without a completion leaves lastCompletionDate null for both origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: preserve last completion across habit cleanup (#391) * Preserve descendant completions during sync cleanup --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix: resolve stored recap week anchors without the current preference A closed week anchor lives in the recap share link and in the stored snapshot key. Validating it against today's WeekStartDay rejected a Monday anchor after the person moved to Sunday, before the stored snapshot could be read. Accept any supported week start (Sunday or Monday) for an explicit closed anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Redesign the transactional emails, AI push copy and error messages (R20) (#532) * Add habit widget empty reason (#527) * Start #372 * Add habit widget empty reason * ORB-223: Generate gating matrix from PayGateService (#520) * chore: start ORB-223 * feat: generate gating matrix * fix: fail closed on unreadable plan gates * fix: close gating matrix parser gaps * fix: preserve plans in combined guards * fix: distinguish quota lifted plans * fix: carry plan provenance through quota aliases Derived quota variables were recorded unscoped and only the variable that literally contains the plan ternary was relabelled afterwards, so plan provenance did not survive a local alias. A semantics-preserving `var selectedLimit = user.HasProAccess ? proLimit : freeLimit; var messageLimit = selectedLimit;` generated cleanly and changed CanSendAiMessage.quotaLiftedByPlan from "Pro" to null. The directly plan-selected variables are now labelled first, and the fixed-point propagation carries the source variable's exact plan instead of null. An assignment deriving from two different plans cannot have its provenance proven, so generation fails closed with the same "cannot derive plan requirement" error the other unprovable shapes use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: fail closed on unknown gating sources * fix: derive feature flags from migrations --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fall back to Claude only when the Codex reviewer step fails (#529) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Redesign the transactional emails, AI push copy and error messages Closes thomasluizon/orbit-tickets#75 (R20). Emails. All 7 types plus the shared wrapper move to the granted canon: the violet #7F46F7 becomes the warm orange #C4530F, the gradient header band and every CTA glow are gone (DESIGN.md bans both), Rubik and Inter become the Geist Sans and Geist Mono stacks, and the planet emoji and sparkle bullets give way to the mark and a plain numbered list. Emails now author light and override dark through prefers-color-scheme, which matches "light mode MANDATORY, dark primary" and survives a forced inversion far better than the dark only document did. Copy is rewritten in both languages to say what happened before what to do next. Support copy moves out of its template into EmailCopy so markup holds no strings. AI push copy. The slip alert, proactive check in and daily summary prompts demonstrated an exclamation mark and a doubled hyphen inside their own worked examples, and at temperature 0.9 the model reproduced both. One shared voice contract, NotificationVoice, now states each ban and carries no example at all. The fallback strings carried the same two tells and are rewritten. Error messages. A domain guard firing is an ordinary event, but its message is the sentence a developer wrote, and on 2026-08-05 a tester read "Days can only be set when frequency quantity is 1." verbatim, in English, inside a pt-BR interface. ErrorCopy now maps every error code to user facing copy in both languages, and one result filter swaps the message at the response boundary. DomainErrors stays developer facing, as the ticket requires. The catalog is total over ErrorCodes, ErrorMessages and DomainErrors, and a reflection test fails the build when a constant arrives without copy, so the raw message fallback is unreachable. ErrorMessages now takes its English from the same catalog rather than repeating 145 sentences in a second file. AppError and Result carry the arguments behind a {0} placeholder so the localized template can be formatted with the same values, which the eagerly formatted message alone no longer exposed. Also removed: five pay gate strings ending in "Upgrade to unlock!", a banned word and an exclamation on a sell; inlined copies of error sentences in three chat tools; and a hardcoded 500 envelope message. Tests: 6980 passing, 0 failing. Line coverage 86.40%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Build the banned dashes from code points and refresh the architecture map Two red checks on PR 532 at c50f052e. Dash Ban. The three new voice tests detected a banned dash by embedding one, and the gate scans a test file like any other. The two constants are now built from their code points, ((char)0x2014) and ((char)0x2013), so no literal character sits in the source. The escape form these files were written with did not survive: the lefthook pre-commit step runs dotnet format, which normalized "—" back into the raw character before the commit landed. A code point arithmetic expression has no literal for a formatter to normalize. The assertions are unchanged. Verified by injecting a real em dash into one ErrorCopy entry and one EmailCopy string: EveryEntryObeysTheErrorVoice and NoEmailStringCarriesADashCharacter both went red, then green again once the injection was reverted. drift. architecture.json and architecture.html were stale against the new test classes and the changed file counts. Regenerated with node tools/arch-map.mjs and committed here, in the change that requires them. The generator is deterministic: two consecutive runs are byte identical. tools/dash-baseline.json is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix the four blocking findings on the email and error copy pass C1: the deletion email promised a permanent wipe while the product only deactivates. The intro now states the grace window, read from AppConstants.MaxDeletionGraceDays, and that signing in again cancels it. C2: the copy pass had moved the wrong-code failure onto a new error code both shipped clients have no branch for, which sends Orbit 1.3.31 and the web step-up screen to a generic error. The code returns to INVALID_VERIFICATION_CODE, and the count rides in a counted copy variant that keeps the trailing token those clients parse. C3: the reply language came from Accept-Language alone, which neither the Android app nor the web Server Actions send, so every signed-in pt-BR reader got English. RequestLanguageResolver reads the stored User.Language first and keeps the header as the anonymous fallback. The header parse now honours quality values. C4: the slip alert claimed the usual time had arrived, while the scheduler sends two hours early and at 08:00 when no peak hour exists. The prompt states when the push lands and the fallback makes no time claim without a peak hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the copy claims match the code that produces them C5: the check-in fallback called one open habit "a few" and promised that Astra records the rest. The scheduler sends as soon as one habit is off track, and LogHabitTool records only the habit a person names. C6: the 429 body was an anonymous object the localizing filter could not match, so the RATE_LIMITED copy was unreachable. It is built from ErrorMessages.TooManyRequests now, which also gives the 429 an error code for the first time. Retry-After and the request id already ride in headers. C8: the sync copy promised a reload and a later send that no caller performs. C9: a challenge does not take part, people do. C10: the recap arrives only for a reader with a push subscription, so the copy points at coming back instead. D1 and D4: two doc comments claimed more than the code does. The filter covers ErrorResponse bodies only, and a FluentValidation message never reaches it. A missing catalog entry throws on the first request, not at startup. D3: BuildSummaryPrompt is internal and joins both voice theory sources, so the guard written for its doubled-hyphen leak now reaches it. D5: AppError compared Args by reference, so two errors formatted with the same count came out unequal. Equality walks the arguments now. The three voice tests that read NotificationVoice.Rules' own literals back out of it now assert the bans over every prompt a model receives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Refresh the architecture map for the review round Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Let the executor own the MCP confirmation gate (#599) (#534) * fix: let the executor own the MCP confirmation gate (#599) Every MCP tool whose capability requires a confirmation or a step-up was refused forever. The selective-auth middleware evaluated the policy a second time, in front of the executor, and it never received the caller's confirmation token. A client that stepped up and retried with a valid token got the identical refusal, because the token never reached that evaluation. Carrying the token into the middleware cannot fix it. A confirmation token is single use and is bound to the pending operation's fingerprint, and the middleware computes a different fingerprint from the raw MCP arguments than the executor computes from the operation id and its snake_case argument object. One token cannot satisfy two gates. So the middleware now steps aside for a confirmation-gated capability, the same way it already steps aside for execute_agent_operation_v2. Both reach IAgentOperationExecutor, which evaluates access and confirmation together, holds the token, and writes the audit row. Two guard tests pin the invariants the deferral rests on: a confirmation requirement always sits on a mutation, and every confirmation-gated MCP tool reaches the executor through McpExecutorBridge. Also corrected: the step-up message and the three tool parameter descriptions named verify_step_up_agent_operation_v2 as the source of the confirmation token. It does not return one. confirm_agent_operation_v2 does. Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and nothing ever read it; the real step-up state lives on PendingAgentOperationState.StepUpSatisfiedAtUtc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: thread the confirmation token through the bulk habit tools (#599) bulk_log_habits and bulk_skip_habits declared no confirmationToken parameter and their shared helper hardcoded confirmationToken: null, so their HabitsBulkWrite capability (FreshConfirmation) could never see a token and both tools stayed permanently refused. Both now declare the parameter and ExecuteBulkHabitOperationAsync forwards it. Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests pin what the old one missed. The scan now asserts it reaches exactly the catalog's gated MCP tools, that each tool's forwarded operation id resolves to a capability with the same ConfirmationRequirement, and that each tool accepts and forwards a confirmation token. The source scan walks subfolders and keys members by their declaration rather than by the first invocation-shaped token in the chunk. AgentOperationExecutor writes an AgentAuditLogs row before returning UnknownOperation, restoring the trail the middleware used to leave. McpConfirmationGateTests now drives the real AgentTools recovery methods end to end and pins that all three refuse an API-key credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599) The token guard whitelisted one spelling of null. It flagged a bridge call only when the confirmation-token argument was absent or the literal `null`, so `confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while the tool stayed refused forever. A longer literal list does not close that, because the next spelling is not on the list either. Assert the structural property instead: a gated tool declares a `string? confirmationToken` parameter, and the expression it forwards into the executor's token slot resolves back to that identifier, directly or through one helper hop. Every other expression fails, whatever it spells. The declaration check now reads the tool's parameter list rather than its whole body, so a local named `confirmationToken` no longer satisfies it. Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It compared only the confirmation requirement, so a gated tool could forward another gated capability's operation id, keep confirmation firing, and have the executor enforce the wrong scope. The file's doc comment said four invariants and listed four; the file holds five. Name the source-scan invariant the other three rest on. Six mutations, each red on its own, real tree green at every step: `confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool forwarding a different local, and `delete_tag` forwarding `delete_goal`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: strip only a real named-argument prefix in the MCP guard parser (#599) ArgumentAt treated any colon as a named-argument separator, so a conditional expression in the token slot or the operation-id slot collapsed to its else-branch. `ids.Count > 0 ? null : confirmationToken` read as `confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read as `"delete_tag"`, and both guards stayed green over the restored defect. Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and every other expression reaches the resolver whole. The anchor keeps a colon inside a string literal and a `::` qualifier from matching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: separate an omitted caller argument from a non-parameter expression (#599) ResolveExpression returned the expression itself both when it was not a callee parameter and when the caller supplied nothing at that index. The second case handed back the helper's own parameter name, which then compared equal to `confirmationToken` and passed. Giving the helper an optional `string? confirmationToken = null` and calling it without that argument restored the #599 defect with the guard green. Return the `<omitted>` sentinel for the second case instead. It can never equal the parameter name, and it resolves to no capability in the operation-id slot, so both guards fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a gated tool's direct and helper bridge calls together (#599) CollectBridgeCalls returned early as soon as the tool body held one bridge call, so a correct call in a dead branch hid every helper call in the same tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken` plus a same-file helper forwarding `null` restored the #599 defect with the guard green. Collect both sets instead. Every bridge call a gated tool can reach, directly or through one helper hop, now has to forward the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: refuse a gated tool that assigns to its own confirmationToken (#599) The rule compared the forwarded expression's text only, so `confirmationToken = null;` above the bridge call restored the #599 defect with the forwarded identifier unchanged and the guard green. Add an offender when the tool body, or a same-file helper it reaches, assigns to the parameter. MemberSource now carries its statements with the parameter list excluded, so the declaration's own `string? confirmationToken = null` default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`, `!=`, `>=`, `<=`, `??=` and a lambda arrow alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the MCP token resolver models, not more (#599) Invariant 5 claimed "any other expression in the token slot refuses the tool forever" and that the tool forwards the identifier "directly or through a helper it calls". A conditional refuses the tool on some paths only, and the resolver models exactly one same-file helper hop matched by argument position. Replace both sentences with what the scan actually does, and state its limits: two or more hops read as no bridge call and fail the routing guard, and aliasing, reflection and an interface call are outside the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: catch a compound assignment to confirmationToken (#599) The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??` of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the bridge call compiled green with all five guards green, and a later change that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would hand the executor a token the MCP caller never sent: HasFreshConfirmation is then satisfied from state the caller does not control, while the middleware has already stepped aside. Admit the two compound operators a `string?` can carry. `==` still fails on the second `=`, `!=`, `>=` and `<=` still fail because those characters break `\s*` and are not in the alternation, `=>` still fails on the trailing character class, and a bare `??` with no `=` still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read every same-named helper an MCP tool could be calling (#599) The member index was a dictionary keyed on the bare name and built with group.First(), so two same-named members collapsed to whichever was declared first. The scan then analysed a method the tool never calls. It broke both ways: a decoy overload declared after the real helper and forwarding null was green, and a gated tool calling a correct overload reddened when an unrelated same-named member happened to be declared first. Hold every member under its name and, at each call site, read every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That fails closed instead of guessing, and it costs no false red in the case above, where only the real helper carries a bridge call. A name that admits no candidate stays unresolved, which hides nothing: the bridge call inside it is not collected either, so the routing guard reddens the tool. Arity, not the parameter type, is what decides a candidate. The scan is a text scan, so it cannot type-check an argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what invariant 5 now refuses and what it still cannot see (#599) The general claim "refuses a tool that assigns to the parameter" is now exact: `=`, `??=` and `+=`. The helper hop no longer claims a "matching argument position", because the resolver pairs by position alone and strips a named-argument prefix without reading the name, so arguments named out of the declared order are read wrong. The comment said nothing about overloads, so say that an ambiguous name is read as every candidate and reddens the tool. Close with the honest limit: a text scan is defeatable by an author who sets out to defeat it, and what the guard closes is every shape an ordinary refactor produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fix calendar event timezone projection (#521) * Start calendar timezone fix * Fix calendar event timezone projection * Fix projected calendar end times * Fix calendar end time during DST fallback * Fix calendar end time minute precision * Fix projected calendar recurrence rule * Guard calendar recurrence across timezone shifts * Project calendar recurrence weekdays * Keep calendar recurrence unchanged * Omit unrepresentable recurring calendar events * Gate projected calendar recurrence on the whole fetch window Closes the six review findings on pull request 521. 1. The weekday gate now walks every expanded occurrence Google returned for a recurring master, not the one sampled instance. The fetcher collects each instance start with the source calendar's own offset into a JsonIgnore'd ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in January and shifts to Wednesday in July is refused. The empty list still falls back to the sampled instance for an all-day series and for a suggestion row read back from the database. 2. The legacy title plus date plus time key only excludes a suggestion when exactly one candidate carries it. Inside a fall-back repeated hour two events project to the same local time, so the key proves nothing and both stay. This mirrors the group.Count() == 1 guard the auto sync reconciler already applies to the same key. 3. An omitted end time now logs its reason at Debug with the event id and the user id. EndUtc already ships the real duration, so the client keeps it. 4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule it imports and cannot re-express without inventing a weekday. 5. Both new call sites pass the logger and the user id to FindTimeZone, which also catches InvalidTimeZoneException so a corrupt zone stops escaping as a 500. User.SetTimeZone trims at the boundary and rejects a blank id. 6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the sub hour offset gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Prove calendar recurrence stability from both zones Round 6 gated a BYDAY series on the expanded instances Google returned, and GoogleCalendarApi only asks for sixty days, so a series whose transition falls outside that window was admitted and a stored suggestion row never carried the instances at all. The gate now reads the source calendar's own timezone from the recurring master the fetcher already fetches, and walks a year of dates at the occurrence's source wall clock through both zones' rules. A series it cannot prove stable is withheld from both feeds. StoredCalendarEventJson carries the source zone beside the stored suggestion, so the suggestion feed judges a row on the same evidence the events feed had. Also in scope: the auto-sync reconciler projects a fetched event into the account timezone before matching a legacy habit, the end-time omission logs whatever EndUtc holds, and an all-day event no longer reports Google's exclusive end date as an end instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep calendar series a spring forward gap cannot move A probe date whose source wall clock a spring forward gap removes is a date the series does not fire on, not evidence the series moves. The walk ended there with "unproved" and the gate withheld the whole series, so an account reading a calendar kept in its own zone lost an hour wide band in thirteen DST zones even though the projection is the identity. HasUnrepresentableRecurrenceAfterProjection now answers false as soon as TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone removes instead of ending. RFC 5545 section 3.3.5 does define the missing case, but probing that normalized instant changes no decision for any of the 14,752 zone pairs that could distinguish it, and asserting an expansion Google does not document would let one guessed date withhold a year. Also pins what earlier rounds argued: auto-sync refusing to store or notify a withheld series, JsonIgnore keeping SourceTimeZone off the response body, the repeated hour needing both of its instants, the probe reaching a full year, and an all day event logging no dropped end time. A stored SourceTimeZone key holding a number now reads as no source zone rather than failing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix recurring calendar timezone proof and legacy refresh * Avoid refreshing legacy calendar rows with duplicate event IDs --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Bump FluentAssertions and 23 others (#538) Bumps FluentAssertions from 8.10.0 to 8.11.0 Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277 Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25 Bumps Hangfire.Core from 1.8.24 to 1.8.25 Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12 Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12 Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12 Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0 Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1 Bumps OpenAI from 2.13.0 to 2.14.0 Bumps PostHog from 2.14.0 to 2.15.7 Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7 Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9 Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1 Bumps Stripe.net from 52.3.0 to 52.4.2 --- updated-dependencies: - dependency-name: Google.Apis.AndroidPublisher.v3 dependency-version: 1.76.0.4277 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Hangfire.Core dependency-version: 1.8.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.AspNetCore.OpenApi dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.ApiDescription.Server dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Http dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: OpenAI dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog dependency-version: 2.15.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: PostHog.AspNetCore dependency-version: 2.9.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Scalar.AspNetCore dependency-version: 2.17.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Sentry.AspNetCore dependency-version: 6.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Stripe.net dependency-version: 52.4.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.Extensions.Caching.Memory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: FluentAssertions dependency-version: 8.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.InMemory dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-minor-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Give API key management one authorization concept with two doors (#528) * Start #529 API key step up * Require API key management step up * chore: regenerate the architecture map The step up change moved the API key endpoints' shape, so `architecture.json` and `architecture.html` no longer matched the tree and the `drift` check failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`, which reports 45 entities and 0 untested feature folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give API key management one authorization concept with two doors Listing, creating and revoking API keys now read one grant. Two doors open it, and both end in a six-digit code emailed to the account owner: the HTTP challenge, and a verified agent step-up. Creating key material spends the grant. Listing and revoking only read it, so one emailed code authorizes a management session instead of exactly one revoke. A revoke no longer locks the person out of the key list. get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read capability to a step-up inside the executor, so the read opens a pending operation the caller can step up against, and the MCP read routes through McpExecutorBridge like the mutation does. AppConfigService now parses a stored row strictly and throws by name when a row is malformed, so a typo cannot leave the gate off while the read-back reports it on. Both controller acti…




Closes #599.
Every MCP tool whose capability carries a confirmation requirement was refused forever, and the retry that should clear the refusal could not. The selective-auth middleware in
src/Orbit.Api/Extensions/WebApplicationExtensions.csevaluated the agent policy a second time, in front ofIAgentOperationExecutor, and it never received the caller's confirmation token.Why carrying the token into the middleware does not work
The ticket asks for
ConfirmationTokento be carried into the middleware'sAgentPolicyEvaluationContext. Reading the two paths field by field shows that cannot deliver the expected behaviour, so this change removes the duplicate gate instead of duplicating the token plumbing into it.PendingAgentOperationStore.TryConsumeFreshConfirmationcallsMarkConsumed()on success. Whichever gate consumes it first leaves nothing for the second gate.Compute(toolName, rawMcpArgumentsJson)over the camelCase MCP arguments. The executor computesCompute(operationId, bridgeArgumentsJson)over the snake_case objectMcpExecutorBridgebuilds. Formanage_api_keysthose differ (keyIdagainstkey_id, and so on), so the middleware's pending operation and the executor's pending operation are two different rows. A token minted against one never matches the other.So the middleware now steps aside for a confirmation-gated capability, exactly as it already steps aside for
execute_agent_operation_v2at the top of the same method. Both kinds of call reachIAgentOperationExecutor, which evaluates access and confirmation together, holds the caller's token, and writes theAgentAuditLogsrow.Blast radius: every StepUp capability reachable from MCP
Three capabilities declare
AgentConfirmationRequirement.StepUp. All three were broken onmain, and all three route throughMcpExecutorBridge, so all three are fixed here.subscriptions.managemanage_subscriptionstep_up_required, no retry possible.api-keys.managemanage_api_keysstep_up_required, no retry possible.account.managemanage_accountstep_up_required, no retry possible.This fixes the JWT and OAuth door only. The API-key door stays closed by design:
AgentTools.cs:56,:73and:89throwUnauthorizedAccessExceptionforAgentAuthMethod.ApiKey, so an MCP client on an API key still cannot mint a confirmation token for any of the 15 gated tools. That behaviour is correct, it fails closed, and this change does not touch it.TheRecoveryTools_RefuseAnApiKeyCredentialSoThatDoorStaysClosednow pins it.The same defect also blocked all twelve
FreshConfirmationMCP tools, refused withconfirmation_requiredand equally unable to retry:delete_habit,bulk_delete_habits,bulk_create_habits,bulk_update_habits,bulk_reschedule_habits,bulk_log_habits,bulk_skip_habits,delete_goal,delete_tag,delete_notification,delete_user_fact,manage_calendar_sync.AgentPlatform:ShadowModeEnabledis absent from everyappsettings*.json, so it defaults tofalseand the refusals were live in production.Ten of those twelve are fixed by removing the duplicate gate alone, because each already declares a
confirmationTokenparameter and forwards it toMcpExecutorBridge.ExecuteAsync.bulk_log_habitsandbulk_skip_habitswere not. They declared no such parameter, and their shared helperExecuteBulkHabitOperationAsynchardcodedconfirmationToken: null, soHasFreshConfirmationcould never returntrueand both stayed permanently refused after the first round's change. Both now declare the parameter with the same[Description(...)]shape as theirHabitsBulkWritesiblings, and the helper forwards it:HabitTools.cs:458(thebulk_log_habitsparameter),:468(thebulk_skip_habitsparameter),:478(the helper parameter) and:489(the bridge call). Those four are read at headf3766d18and are unchanged by round 3; an earlier revision of this body cited:469and:479, which are the twoCancellationTokenlines.Every hardcoded
confirmationToken: nullinHabitTools.cs, one verdict eachnullis correct wherever the forwarded operation's capability declaresAgentConfirmationRequirement.None, because no confirmation is ever evaluated for it. Line numbers are the review's, at head9c7d8d56.:132create_habithabits.writeNonenullcorrect:166update_habithabits.writeNonenullcorrect:197log_habithabits.writeNonenullcorrect:236skip_habithabits.writeNonenullcorrect:254update_checklisthabits.writeNonenullcorrect:341create_sub_habithabits.writeNonenullcorrect:357duplicate_habithabits.writeNonenullcorrect:486bulk_log_habits,bulk_skip_habitshabits.bulk.writeFreshConfirmation:489).:502reorder_habitshabits.writeNonenullcorrect:518move_habit_parenthabits.writeNonenullcorrect:541link_goals_to_habithabits.writeNonenullcorrectThe last row was not among the review's ten; it carries the same verdict as the other
habits.writesites. The same sweep over the other eleven files insrc/Orbit.Api/Mcp/Tools/found no further instance. Every one of the remaining fourteen gated MCP tools already threads the caller's parameter, and every otherconfirmationToken: nullin that folder sits on a capability declaringNone. Guard 5 below now enforces that instead of leaving it to a reader.Field by field against the chat path
OperationFingerprintEvaluateAccessRequirementsdenies a mutation with a blank fingerprint.OperationArgumentsJsonEvaluateConfirmationRequirement, to seed a pending operation's arguments. The middleware no longer reaches that branch, so passing it would be dead.ConfirmationTokenconfirmationTokenparameter.StepUpSatisfiedPendingAgentOperationState.StepUpSatisfiedAtUtc, whichAgentStepUpService.VerifyChallengeAsyncwrites andIsUsablereads. The chat surface does not set this field either, so there was no second mechanism to copy.ConfirmationRequirementOverrideAgentPolicyEvaluationContext; the record declares five, and this is not among them. Nothing to carry or omit.What the MCP surface does when the token is absent
It refuses and names the recovery path, through
McpExecutorBridge.BuildPendingMessage. That message and the three tools'confirmationTokendescriptions all namedverify_step_up_agent_operation_v2as the source of the token. It does not return one: it returns aPendingAgentOperationand marksStepUpSatisfiedAtUtc. Onlyconfirm_agent_operation_v2mints a token, viaPendingAgentOperationStore.Confirm. All four strings now name the real four-step recovery:step_up_agent_operation_v2, thenverify_step_up_agent_operation_v2, thenconfirm_agent_operation_v2for the token, then retry the tool with it.StepUpTool_AfterTheFourRecoveryToolsRun_RunsInsteadOfBeingRefuseddrives that exact sequence through the realAgentToolsmethods rather than through the services under them, and asserts the retry runs.Five guard tests hold the deferral safe
Skipping the middleware gate is only safe while every confirmation-gated MCP tool actually reaches the executor and arrives there carrying a gate the executor can enforce.
tests/Orbit.Infrastructure.Tests/Mcp/ConfirmationGatedMcpToolsRouteThroughExecutorTests.cspins all of it.EveryConfirmationRequirement_SitsOnAMutation. Only a mutation is guaranteed to reach the executor.TheSourceScan_ReachesEveryConfirmationGatedMcpToolInTheCatalog. The scan's own tripwire. It asserts the set of tools the source scan found equals the set of gatedmcpToolsin the catalog, so a scan that quietly stopped finding anything cannot turn the three guards below green while enforcing nothing.EveryConfirmationGatedMcpTool_RoutesThroughTheExecutorBridge. Every gated tool reachesexecutorBridge.ExecuteAsync, following one level into a same-file helper so the shared bulk-habit wrapper counts.EveryConfirmationGatedMcpTool_ForwardsAnOperationIdCarryingTheSameConfirmationRequirement. The hole the first round left open. The old guard only asked whether the textexecutorBridge.ExecuteAsync(appeared, never which operation id was passed, and the executor gates oncatalogService.GetCapability(operation.CapabilityId), not on the tool name. The two can diverge. Swapdelete_notification's forwarded"delete_notifications"for the file's ownUpdateNotificationsToolconst and three things happen together: the tool name still resolves to a gated capability so the middleware still steps aside, a bridge call is still present so the old guard stays green, and the executor gates onNotificationsWrite, which declaresNone. The tool would then run with no confirmation at all. The new guard resolves the forwarded operation id out of the source (a string literal, a same-fileconst string, or a helper parameter bound positionally from the caller's argument) and asserts the capability it resolves to is the tool's own: sameIdand sameConfirmationRequirement. Round 3 added theIdhalf. Matching the requirement alone was not enough, because a gated tool could forward a different gated capability's operation id and stay green: swappingTagTools.cs:82from"delete_tag"to"delete_goal"leaves both atFreshConfirmation, so confirmation still fires, but the executor then enforcesgoals.deletein place oftags.delete.EveryConfirmationGatedMcpTool_AcceptsAndForwardsAConfirmationToken. Pins thebulk_log_habits/bulk_skip_habitsdefect so it cannot return: each gated tool must declare astring? confirmationTokenparameter, and that bare identifier must be what reaches the bridge's token slot. The resolver is a text scan over one file, not a compiler. It accepts the identifier written directly in the bridge call, or carried through exactly one same-file helper, and it refuses a tool that writes to the parameter with=,??=or+=before the call. A conditional expression, an omitted optional argument,default,null!,(string?)null,""and an unrelated local all fail, because none of them is that identifier. The helper hop pairs a caller argument with a helper parameter by position alone, and it strips a named-argument prefix without reading the name. Several members can share one name, so the scan reads every member whose parameter count can admit the call: an ambiguous name reddens the tool instead of resolving to one of them. Two or more helper hops read as no bridge call at all and fail guard 3 above; that direction is safe, so it stands. Aliasing, reflection and a call reached through an interface are outside what the scan models, and an author who sets out to defeat a text scan can spell the bypass another way. What this guard closes is every shape an ordinary refactor produces.Round 3 replaced a weaker rule here. The guard first flagged a call only when the token argument was absent or the literal text
null, which whitelisted one spelling:confirmationToken: default,null!,(string?)nulland""are allnullfor astring?and all passed, leaving both bulk tools permanently refused with the guard silent. Addingdefaultto the literal list would not close it, because the next spelling is not on the list either. Asserting the structural property instead - the forwarded expression is the tool's own parameter - refuses every spelling in one rule, including an unrelated local. The declaration check also now reads the tool's parameter list rather than its whole body, so a local namedconfirmationTokenno longer satisfies it. This is the structural guarantee that made the guard preferable to hand-enumerating eachconfirmationToken: nullsite, so a one-spelling whitelist was weaker than the claim above it. Round 4 then fixed the parser under that rule. The rule is right; four defects in its helpers let five mutations compile, restore the Reduce daily summary log egress for ticket 763 #599 defect and leave all five guards green. Section 9 shows each one.Two scanner defects are fixed in the same file.
Directory.EnumerateFilesusedSearchOption.TopDirectoryOnly, so any subfolder added underMcp/Toolswas skipped silently; it now walksAllDirectories.BuildMemberBodieskeyed a member by the first\w+(token in its chunk, which is an invocation pattern, not a declaration. That mis-keys any member the boundary regex does not split on, including one written without an explicit access modifier, and it dropped a generic method outright:McpToolHelpers.DeserializeJson<T>was absent from the lookup at the previous head, because>sits between the name and the(. Members are now keyed from the declaration itself. This one produces a false red rather than a false green, so it was the lower risk of the two, but it was still wrong.Together with the existing
McpToolsDoNotDispatchCommandsguard, a future tool that skipped the bridge, forwarded the wrong operation id, or dropped its token turns one of these red instead of silently losing its gate.An unknown operation leaves an audit row again
AgentOperationExecutor.ExecuteAsyncreturnedUnknownOperationbefore anyTryAuditAsync, so once the middleware stepped aside a gated tool naming a missing operation left noAgentAuditLogsrow where the legacy middleware path used to write one. A trail gap, not a gate gap, but it is this change that opened it.DenyUnknownOperationAsyncnow writes the row first: capability idunknown, riskLow, policy decisionDenied, outcomeUnsupportedByPolicy, errorunsupported_by_policy, with the arguments redacted the same way every other row is.AuditContextcarries a capability id and a risk class instead of anAgentCapability, because an unknown operation has no capability to carry.Test evidence
Commands run from the worktree root. Sections 1 to 4 are round 1 and still hold. Sections 5 and 6 are round 2. Sections 7 and 8 are round 3, at head
db92bcec. Sections 9 and 10 are round 4, at head2a455864. Sections 11 and 12 are round 5, at this head.1. Existing tests, unchanged, with the defect present. Source reverted to
b6a92960, both new test files removed.dotnet test tests/Orbit.Infrastructure.Tests --filter "FullyQualifiedName~Mcp|FullyQualifiedName~AgentPolicyEvaluatorTests|FullyQualifiedName~AgentStepUpServiceTests"Result:
Com falha: 0, Aprovado: 263. All 263 pass. No existing test caught the defect, including the MCP middleware tests, the policy evaluator tests and the step-up service tests.2. New tests with the defect still present. Full fix restored, then only the capability skip removed from
HandleMcpToolCallAsync.dotnet test tests/Orbit.Infrastructure.Tests --filter "FullyQualifiedName~McpConfirmationGateTests|FullyQualifiedName~ConfirmationGatedMcpToolsRouteThroughExecutorTests"Result:
Com falha: 3, Aprovado: 3. The three that fail are the three that drive the gate, and each fails for the intended reason:The middleware wrote the JSON-RPC policy error and the tool never ran, which is the defect exactly.
ToolWithoutConfirmationRequirement_IsStillGatedByTheMiddlewareand both guard tests pass in this state, so the red is specific to the gate.3. The same tests after the fix. Same command:
Com falha: 0, Aprovado: 6.4. Whole suite after the round 1 fix.
dotnet test Orbit.slnxMcpConfirmationGateTestsdrives the production path rather than a stand-in: the realWebApplicationExtensions.HandleMcpToolCallAsync, the realMcpExecutorBridge,AgentOperationExecutor,AgentPolicyEvaluator,PendingAgentOperationStoreandAgentStepUpServiceover the real capability catalog, with the six-digit code read out of the email service the way a user reads it. Only the backingIAiTooland the ownership lookup are stubs, because the subject is authorization, not the revoke itself.Round 1 ran before the rename, so its output names
StepUpTool_WithConfirmedStepUpToken_RunsInsteadOfBeingRefused. That test is nowStepUpTool_AfterTheFourRecoveryToolsRun_RunsInsteadOfBeingRefusedand drives the fourAgentToolsmethods directly.5. Round 2. Each new assertion was broken on purpose and watched go red, then restored and watched go green. The two guards read the tool sources off disk, so a mutation to
src/turns them red with no rebuild.5a. The P2 hole, driven exactly as the review describes it.
NotificationTools.cs:130changed from"delete_notifications"to the file's ownUpdateNotificationsToolconst, which is"update_notifications".dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter "FullyQualifiedName~ConfirmationGatedMcpToolsRouteThroughExecutorTests"The other four stayed green, including
EveryConfirmationGatedMcpTool_RoutesThroughTheExecutorBridge, which is the old guard: a bridge call was still present, so it saw nothing wrong. That is the hole, reproduced. Aftergit checkout -- src/Orbit.Api/Mcp/Tools/NotificationTools.cs, same command:Com falha: 0, Aprovado: 5.5b. The P1, with the fix reverted.
HabitTools.csrestored to head9c7d8d56, same command:It names both broken tools and nothing else. With the fix restored:
Com falha: 0, Aprovado: 5. Round 3 rewrote this guard's failure message, so re-running this mutation today prints the round 3 wording shown in section 7 rather than the text quoted here.5c. The restored audit row.
DenyUnknownOperationAsyncswapped back for the barereturn AgentOperationResponseFactory.UnknownOperation(...).dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter "FullyQualifiedName~AgentOperationExecutor_ReturnsUnsupportedWhenOperationIsMissing"Restored:
Com falha: 0, Aprovado: 1.6. Whole suite at round 2's head,
f3766d18.dotnet build Orbit.slnx -v q --nologosucceeds with 0 errors, thendotnet test Orbit.slnx --no-build:6255 tests, zero failures, up 4 from round 1's 6251: three new guards plus the API-key refusal test.
CSC : warning CS9057confirms the local SDK still skipsOrbit.Analyzers, so ORBIT0001 to ORBIT0005 did not run locally; every added line was grepped by hand and carries no//comment that is not an XML doc.node tools/check-suppression-allowlist.mjsreports all 101 declared sites accounted for, and this round adds no suppression.node tools/arch-map.mjsregeneratesarchitecture.jsonandarchitecture.htmlwith no drift.7. Round 3. Six mutations, each red on its own, real tree green before every one of them. Command throughout, run in the foreground:
dotnet test tests/Orbit.Infrastructure.Tests/Orbit.Infrastructure.Tests.csproj --no-build --filter "FullyQualifiedName~ConfirmationGatedMcpToolsRouteThroughExecutorTests"Each mutation was also compiled (
dotnet build src/Orbit.Api/Orbit.Api.csproj -v q --nologo,0 Erro(s)every time), so none of the six is a proof against code that would not build. Each was reverted withgit checkout -- <file>and the tree re-run green before the next.7a to 7d. The P2 hole, one spelling of
nullat a time.HabitTools.cs:489, the shared bulk helper's bridge call. Real tree before each:Com falha: 0, Aprovado: 5.:489}, confirmationToken: default, cancellationToken);Com falha: 1, Aprovado: 4bulk_log_habits,bulk_skip_habitsforward'default'}, confirmationToken: null!, cancellationToken);Com falha: 1, Aprovado: 4'null!'}, confirmationToken: (string?)null, cancellationToken);Com falha: 1, Aprovado: 4'(string?)null'}, confirmationToken: "", cancellationToken);Com falha: 1, Aprovado: 4'""'All four fail
EveryConfirmationGatedMcpTool_AcceptsAndForwardsAConfirmationTokenand nothing else. Full text of the first:At the previous head all four were
Com falha: 0, Aprovado: 5. That is the defect this round closes.7e. A gated tool that declares the parameter and then forwards a different local.
TagTools.DeleteTaggivenvar token = confirmationToken;and passingtokento the bridge. This is the direct-call path, not the helper path, so it also shows the rule holds without a hop.7f. The P3, the operation-id swap.
TagTools.cs:82,"delete_tag"changed to"delete_goal". Both capabilities areFreshConfirmation, so this was green before round 3.8. Whole suite at round 3's head,
db92bcec.dotnet build Orbit.slnx -v q --nologosucceeds with0 Erro(s), thendotnet test Orbit.slnx --no-build:6255 tests, zero failures, unchanged from round 2: this round tightens three existing assertions and adds no test. Round 3 touches one file,
tests/Orbit.Infrastructure.Tests/Mcp/ConfirmationGatedMcpToolsRouteThroughExecutorTests.cs, sosrc/is untouched, no suppression is added andarchitecture.jsonreports no drift.CSC : warning CS9057still confirms the local SDK skipsOrbit.Analyzers, so ORBIT0001 to ORBIT0005 did not run locally; every added line was grepped by hand for a bare//or/* */outside///and none is present.One assertion deleted.
McpConfirmationGateTests.cs:116heldoutcome.ToolOutput.Should().NotBeNull();, which cannot fail under a real defect because the fourShould().Contain(...)calls after it already throw on a null string. It is gone.9. Round 4. Five parser bypasses, each red on its own, the real tree green before and after every one of them. Round 3's rule is the right shape, but the parser helpers under it were not load-bearing until round 3 made them so. Five mutations compiled, restored the #599 defect and left all five guards green. Same foreground command as section 7, with
dotnet build Orbit.slnx -v q --nologoreporting0 Erro(s)before every run, andgit checkout -- <file>plus a green re-run between them.HabitTools.cs:489as}, ids.Count > 0 ? null : confirmationToken, cancellationToken);Com falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4bulk_log_habitsandbulk_skip_habitsforward'ids.Count > 0 ? null : confirmationToken'TagTools.cs:82asExecuteAsync(user, tagId.Length > 0 ? "delete_goal" : "delete_tag", ...)Com falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4delete_tag (tags.delete, FreshConfirmation)forwards an operation resolving to an unknown capabilityExecuteBulkHabitOperationAsyncgivenstring? confirmationToken = null, called frombulk_skip_habitswithout that argumentCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4bulk_skip_habitsforwards'<omitted>'if (tagId.Length == 0)branch, with the real path sent to a same-file helper forwardingnullCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4delete_tagforwards'null'confirmationToken = null;above theTagTools.DeleteTagbridge callCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4delete_tagassigns toconfirmationTokenbefore the executor reads itFour root causes, one commit each, all in the guard's own parser.
ArgumentAtread any:as a named-argument separator, so a conditional in either slot collapsed to its else-branch. One defect defeated two guards, because the token slot and the operation-id slot share that method. It now strips a^\w+\s*:(?!:)prefix only, which leaves a colon inside a string literal and a::qualifier alone.ResolveExpressiongave one answer to two questions. "Not a parameter" correctly returned the expression; "the caller passed nothing at that index" returned the helper's own parameter name, which then compared equal toconfirmationToken. The second case now returns an<omitted>sentinel, which can never equal the parameter name and resolves to no capability.CollectBridgeCallsreturned as soon as the tool body held one bridge call, so a decoy hid every helper call in the same tool. It now reads the direct and helper sets together.\bconfirmationToken\s*=[^=>]over the tool's statements, and over a same-file helper it reaches, now adds an offender. The parameter list is excluded so the declaration's own= nulldefault does not match, and==,!=,>=,<=,??=and a lambda arrow do not trip it.9a. The six earlier mutations still redden. Each applied to
TagTools.DeleteTag, compiled with0 Erro(s), run, then reverted:confirmationToken: default,null!,(string?)null,"", a forwardedvar tokenlocal, and"delete_tag"swapped for"delete_goal". All six printCom falha: 1, Aprovado: 4, the first five on the token guard and the last on the operation-id guard. A fix that broke an existing guard would not be a fix.9b. Both legitimate shapes still pass, and the two-hop shape is disclosed. A tool whose only bridge call is direct is the real
delete_tag, green.delete_tagrefactored onto one same-file helper that receives and forwardsconfirmationTokencompiles and stays green, so reading direct and helper calls together did not cost the one-hop refactor. Two helper hops go red onEveryConfirmationGatedMcpTool_RoutesThroughTheExecutorBridge: the resolver follows exactly one hop, so it sees no bridge call at all. That is a false red, not a false green, so it is left as is.10. Whole suite at round 4's head,
2a455864.dotnet build Orbit.slnx -v q --nologosucceeds with0 Erro(s), thendotnet test Orbit.slnx --no-build:6255 tests, zero failures, unchanged from rounds 2 and 3: round 4 fixes four parser helpers and one doc block, and adds no test. It touches the same single file,
tests/Orbit.Infrastructure.Tests/Mcp/ConfirmationGatedMcpToolsRouteThroughExecutorTests.cs, sosrc/is untouched, no suppression is added andarchitecture.jsonreports no drift.CSC : warning CS9057still confirms the local SDK skipsOrbit.Analyzers, so ORBIT0001 to ORBIT0005 did not run locally; every added line was grepped for a bare//or/* */outside///and none is present.11. Round 5. Two last bypasses, one false red removed, and the prose cut back to what the code does. Round 4's thirteen fresh attacks found nine already closed, including a colon and a comma inside a string literal,
global::in the call, aconfirmationTokenBackuplocal,==and>=near the assignment check, three direct bridge calls, and a helper shared by a gated and an ungated tool. Two got through. Every run below is agit archiveextract of the head under test, withdotnet build src/Orbit.Api/Orbit.Api.csprojreporting0 Erro(s)before each guard run and the tool sources restored from a pristine copy between runs.2a455864confirmationToken ??= tagId;above theTagTools.DeleteTagbridge callCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4delete_tagassigns toconfirmationTokenbefore the executor reads itconfirmationToken += string.Empty;above the same bridge callCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4ExecuteBulkHabitOperationAsyncoverload holding its ownnulltoken forward, declared after the real helperCom falha: 0, Aprovado: 5Com falha: 1, Aprovado: 4bulk_log_habitsandbulk_skip_habitsforward'null'Com falha: 1, Aprovado: 4Com falha: 1, Aprovado: 4ExecuteBulkHabitOperationAsyncmessage helper declared before the real helper, and called by itCom falha: 1, Aprovado: 4, a false red on guard 3Com falha: 0, Aprovado: 5Two root causes, one commit each, both in the guard's own parser.
The assignment rule missed every compound assignment.
\bconfirmationToken\s*=[^=>]anchors on\s*, which cannot cross the??of??=or the+of+=.confirmationToken ??= tagId;compiled green with all five guards green, and that is not a contrived line: a later change writingconfirmationToken ??= await ResolveStoredTokenAsync(user, cancellationToken);hands the executor a token the MCP caller never sent, soHasFreshConfirmationis satisfied from state the caller does not control while the middleware has already stepped aside. The rule is now\bconfirmationToken\s*(?:\?\?|\+)?=[^=>], which admits the only two compound operators astring?can carry.==still fails on the second=,!=,>=and<=still fail because those characters break\s*and are not in the alternation,=>still fails on the trailing character class, and a bare??with no=still fails.The member index collapsed overloads. It was a dictionary keyed on the bare name and built with
group.First(), so two same-named members became whichever was declared first, and the scan then analysed a method the tool never calls. It broke in both directions, and declaration order was the only thing that decided it.The lookup now holds every member under its name, and each call site reads every candidate whose parameter count can admit that many arguments. One candidate resolves the call; more than one is ambiguous, so all of them are read and any bad one reddens the tool. That is the fail-closed option rather than the key-on-arity-alone option, chosen because the false red is the nastier half of this defect and arity alone would still have to pick one member when two overloads share an argument count. The cheap arity filter is what removes the false red in the table's last row, where the same-named sibling cannot take seven arguments. A name that admits no candidate stays unresolved, and that hides nothing: the bridge call inside it is not collected either, so guard 3 reddens the tool. Arity, not the parameter type, decides a candidate, because a text scan cannot type-check an argument.
11a. The four ordered negatives still pass, because a fix that reddens correct code is not a fix. Each compiles with
0 Erro(s)and printsCom falha: 0, Aprovado: 5:if (confirmationToken == null && tagId.Length >= 1 && confirmationToken != tagId)inDeleteTag; astring? confirmationTokenBackup = null;local beside a correctconfirmationToken: confirmationTokennamed argument; aDescribeTokenshelper reached fromDeleteTagwhose body holds the lambdaconfirmationToken => confirmationToken ?? "none"; and the correct-overload row in the table above, where the ambiguity is resolved by arity rather than guessed.11b. All eleven earlier mutations still redden. Round 4's five - the conditional in the token slot, the conditional in the operation-id slot, the optional-helper-parameter refactor, the decoy direct call beside a helper forwarding
null, andconfirmationToken = null;- plusconfirmationToken: default,null!,(string?)null,"", a forwardedvar tokenlocal and thedelete_tagtodelete_goalswap. All eleven compile and all eleven printCom falha: 1, Aprovado: 4.11c. Three limits are known and deliberately left. A named argument is stripped without checking its name, so a helper call written
..., confirmationToken: null, filterJson: confirmationToken, ...stays green while C# binds the token intofilterJson; nobody reorders two named arguments by accident, and it is confined to the helper hop because the bridge signature has onestring?slot. A correct call reddens when its trailing named arguments are reordered.SplitTopLevelArgumentsdoes not track<and>, so anew Dictionary<string, string>payload garbles the failure message. The last two fail closed, and the first needs a name-aware lookup that the arity filter did not make cheap. The doc comment now states the first two rather than claiming more than the code does.12. Whole suite at this head.
dotnet build Orbit.slnx -v q --nologosucceeds with0 Erro(s), thendotnet test Orbit.slnx --no-build:6255 tests, zero failures, unchanged from rounds 2, 3 and 4: round 5 fixes one regex, one lookup and one doc block, and adds no test. It touches the same single file,
tests/Orbit.Infrastructure.Tests/Mcp/ConfirmationGatedMcpToolsRouteThroughExecutorTests.cs, sosrc/is untouched, no suppression is added andarchitecture.jsonreports no drift.CSC : warning CS9057still confirms the local SDK skipsOrbit.Analyzers, so ORBIT0001 to ORBIT0005 did not run locally; every added line was grepped for a bare//or/* */outside///and none is present.Assumptions
capability.ConfirmationRequirement, not on a list of executor-routed tool names. Rejected alternative: a hand-maintained set of the 40 bridge-routed tools, likeAiBearingMcpTools. That set would drift, and the two guard tests make the requirement-based rule self-enforcing instead.StepUpSatisfiedis deleted rather than wired up. Rejected alternative: setting it in both call sites and reading it inAgentPolicyEvaluator. That would be a second step-up mechanism besideStepUpSatisfiedAtUtc, which the ticket explicitly warns against.tests/Orbit.Infrastructure.Tests/Extensions/besideWebApplicationExtensionsMcpTests, and the architecture guard intests/Orbit.Infrastructure.Tests/Mcp/besideMcpToolsDoNotDispatchCommandsTests. Rejected alternative: one file holding both, which would mix a behaviour test with a source-scanning guard.HandleMcpToolCallAsyncandMcpToolCallRequestwiden fromprivatetointernalso the test can drive the real entry point.Orbit.Apialready grantsInternalsVisibleTo("Orbit.Infrastructure.Tests"). Rejected alternative: reaching it over a hosted ASP.NET pipeline, which the unit-only suite does not host.architecture.jsonandarchitecture.htmlare regenerated bynode tools/arch-map.mjsand shipped in the same commit, because the generator picked up the new test class. Round 2 adds no new test class, so the generator reports no drift and neither file changes again.confirmationTokenidentifier itself, not merely a value derived from it. A tool writingvar token = confirmationToken;and forwardingtokenfails, even though it is behaviourally correct. That strictness is the point: the alternative is deciding, from source text, whether an arbitrary expression happens to evaluate to the caller's token, which is the enumeration problem that letdefaultthrough. Forwarding the parameter is what every one of the fifteen gated tools already does, and the failure message says exactly what to write.GetCapabilityByChatTool(tool.Name)at startup, and the unit suite instantiates noIAiToolset. Rejected alternative: asserting over every string literal in the tool's body. Three gated tools nameconfirm_agent_operation_v2in a[Description(...)], and that is itself a chat tool onChatInteractwithNone, so the loose rule would have gone red on tools that are correct.Follow-up found but not fixed here
An allowed MCP mutation now writes one accurate
AgentAuditLogsrow from the executor. Before this change it wrote two: the executor's row plus a lower-fidelity legacy row fromExecuteAuthorizedMcpToolCallAsync, which reportsAllowed/Succeededwithout seeing the tool's real outcome. That duplicate still happens for every non-confirmation MCP mutation, and predates this ticket. It is a separate defect in a separate concern and is not widened into this change.A gated tool that reaches
McpExecutorBridgethrough two or more same-file helper hops is refused byEveryConfirmationGatedMcpTool_RoutesThroughTheExecutorBridge, because the resolver follows exactly one hop and then finds no bridge call. No tool insrc/Orbit.Api/Mcp/Tools/has that shape today. It fails closed, so deepening the resolver is a separate change rather than part of this one.Three narrower limits of the same text scan are listed in section 11c and left for the same reason: the named-argument prefix is stripped without reading the name, a correct call reddens when its trailing named arguments are reordered, and
SplitTopLevelArgumentsdoes not track<and>. Two of the three fail closed. The guard's doc comment now states these limits and says plainly that a source-text scan is defeatable by an author who sets out to defeat it, so the next reader does not mistake it for an airtight rule.🤖 Generated with Claude Code