Align card lifecycle proposal side effects and audit receipts - #3001
Conversation
…ysis archive-lifecycle and restore-lifecycle flip a card's archived state on apply, but SideEffectAnalyzer.CardMutatingActions omitted both names, so the review side-effects Cards row reported "No board mutations" for the write being approved. Add both to the action set and cover them at both entry points.
…estore Applying an archive-lifecycle or restore-lifecycle proposal persisted two contradictory rows: CardService.SetArchivedAsync staged its own typed Archived/Unarchived receipt, and ExecutionAuditRecorder - whose action map did not know the lifecycle action names - added a second row classified as Updated. Map both names in the recorder, prefix the legacy CardService wording onto the provenance string so the single row keeps its existing meaning, and have the proposal lane ask SetArchivedAsync not to stage its own receipt. The direct API archive/restore lane keeps its actor-stamped single receipt unchanged.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3773ae9dfe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| { | ||
| "create", "move", "archive", "update", "delete", "bulk_move" | ||
| "create", "move", "archive", "update", "delete", "bulk_move", | ||
| "archive-lifecycle", "restore-lifecycle" |
There was a problem hiding this comment.
Include restores in the Cards side-effect copy
When a proposal contains only restore-lifecycle, adding it to this generic mutation set routes the Cards row to Creates, moves, or archives cards on the board, even though Apply restores the card. The frontend renders this API value verbatim, so the review trust gate describes the wrong lifecycle direction; use neutral mutation wording or distinguish restore operations, and assert the resulting user-facing value rather than only checking that it differs from No board mutations.
AGENTS.md reference: AGENTS.md:L96-L97
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Triaged once: real, non-blocking (review-time copy; the proposal diff already shows Archived true -> false). Tracked as #3008.
|
Fresh-context independent review (round 1) at exact head Verified clean by the reviewer from source:
Fix-now (classified HIGH for this PR because the row it removes was the always-correct receipt): LOW, recorded (law 2c), no commits requested:
Informational: PR #2977 merged after this head and touches |
…epends on Review of #3001 raised that ExecutionAuditRecorder keys the surviving lifecycle receipt on operation.TargetId while the handler mutates parameters.cardId, so a crafted proposal could archive one card and stamp the receipt on another now that the CardService row is gone. Measured: the attack is not reachable. ProposalOperationContractValidator already rejects any card-targeted operation whose targetId disagrees with parameters.cardId, and AutomationPolicyEngine.ValidatePermissionsAsync runs that validator on BOTH the approve path and the execute path - the latter against the revision-materialized effective operations, so a post-approval revision is caught too. End to end the crafted proposal is created, then refused at approve with 400 and can never be applied. No production change. Add the regression tests that guard is now load-bearing for: a validator unit test (mismatch rejected, match accepted, absent targetId accepted, both lifecycle actions) and an API test walking the full crafted-proposal pipeline and asserting nothing is archived and no receipt lands on the decoy card.
Review round 2 (final round)New head: HIGH finding — targetId / cardId divergence: measured, not reachable. Declining the code change.The concern is correct in shape and worth raising: It cannot diverge. if (cardId.HasValue && targetId.HasValue &&
operation.TargetType.Equals("card", StringComparison.OrdinalIgnoreCase) &&
cardId != targetId)
{
return Result.Failure(ErrorCodes.ValidationError, "Operation targetId must match parameter 'cardId'");
}The review comment looked at the action-specific lifecycle branch (which indeed only requires
I ran the described attack end to end rather than reasoning about it. Board writer, two real cards,
So Apply is unreachable and no misattributed receipt is ever written. Adding a second rejection in the What I did add, because this guard is now load-bearing for the single-receipt design and was not
Both fail if the guard is loosened, which is the protection the finding was actually asking for. LOW — receipt attribution (recorded, not fixed here)The single lifecycle receipt stamps Checks run at this head
Not verified this round: no frontend run (this round is backend tests only and adds no production |
|
Round 2 (fix round) disposition at exact head The round-1 fix-now item (receipt keyed on Triage of everything raised since round 1 (law 2c):
Author-run proof at this head: full |
What
Aligns the two card lifecycle proposal actions (
archive-lifecycle,restore-lifecycle) with therest of the proposal vocabulary in the two places PR #2932's review found them missing.
SideEffectAnalyzer.CardMutatingActionsis an exact-value set thatomitted both lifecycle names, so
/side-effectsreportedNo board mutationsin the Cards rowfor a proposal that archives or restores a card on Apply. Both names are now in the set.
CardService.SetArchivedAsyncstaged its own typedArchived/Unarchivedreceipt, andExecutionAuditRecorder— whose action map did not know the lifecycle action names — added asecond row classified as
Updated. The apply lane now emits exactly one correctly typed row:the recorder maps both names, prefixes the legacy
CardServicewording onto its provenancestring so the row keeps its existing meaning, and the lifecycle handler passes
recordLifecycleAudit: falseso the service does not stage a duplicate.The direct API lane (
POST /api/boards/{b}/cards/{c}/archive|restore) is unchanged: it keeps itssingle actor-stamped receipt with the exact legacy wording. Legacy
archive(Block semantics) isuntouched, and explicit approve + explicit apply are unchanged.
Why
Closes #2939. Follow-up to the two review comments on PR #2932
(
3982485234,3982485243) atcd5ff22.Both are trust-surface defects: the review disclosure understated the write being approved, and the
applied history contradicted itself about what happened.
Overlap note (PR #2977)
#2977 (card assignments) also edits
CardService.csandOperationHandlerRegistry.cs. This PR keepseach to one minimal, non-adjacent hunk, as declared in the lane claim on #2939:
CardService.cs— one hunk inSetArchivedAsync(~L49-L100). Add multiple card assignments and explicit import mapping #2977 touchesMapToDto(~L610).OperationHandlerRegistry.cs— one hunk inSetCardArchivedAsync(~L120). Add multiple card assignments and explicit import mapping #2977 touches theconstructor and
ExecuteOperationAsync(L18-L80).No other file from #2977's set (
AutomationExecutorService.cs,AutomationProposalService.cs,ProposalOperationContractValidator.cs) is touched here.Tests
SideEffectAnalyzerTests— both lifecycle actions (and mixed casing) make the Cards row active atboth entry points, plus a guard that the action only counts when the target really is a card.
ExecutionAuditRecorderTests— both names map toArchived/Unarchived, the single row carriesthe proposal id, sequence and
RequestedByUserId, andBuildAuditChangeskeeps the legacylifecycle wording ahead of the provenance (and does not add it for non-lifecycle actions).
CardServiceTests—recordLifecycleAudit: falsestages no service-level receipt for eitherdirection; the existing test still pins the default actor-stamped receipt.
AutomationProposalsApiTests.CardLifecycleAudit_ProposalApplyAndDirectApi_EachPersistExactlyOneTypedReceipt— end-to-end persisted-audit regression: proposal-applied archive and restore each leave
exactly one typed row with proposal provenance and no
Updatedfallback row, while the sametwo transitions through the direct API leave exactly one actor-stamped row each with the unchanged
legacy wording.
Regression proof (run against this branch's tests with the production files reverted to
origin/main): the API test fails on theNo board mutationsassertion with the analyzerreverted, and fails on the
proposalAuditsduplicate assertion with only the recorder/handlerreverted. Both assertions are real guards, not tautologies.
Checks run
dotnet test backend/tests/Taskdeck.Application.Tests/... --filter "FullyQualifiedName~SideEffectAnalyzerTests|ExecutionAuditRecorderTests|CardServiceTests"dotnet test backend/tests/Taskdeck.Api.Tests/... --filter "FullyQualifiedName~CardLifecycleAudit_ProposalApplyAndDirectApi"dotnet test backend/Taskdeck.sln -c Release -m:1(required backend gate)node scripts/check-docs-governance.mjsnode scripts/check-doc-links.mjsDocs
One sentence added to
autodoc/interfaces/proposal-operation-vocabulary.mdon thearchive-lifecycle/restore-lifecyclebullet, recording the side-effect classification and thesingle-receipt contract.
docs/product/documents neither audit receipts nor the side-effect rows,so nothing there needed a change;
docs/STATUS.mdis untouched (no shipped-capability change — thiscorrects disclosure and history for an already-shipped operation).
NOT verified
same text; only the branch it takes for lifecycle-only proposals changes. Not exercised in a
browser or Playwright.
archive_card_lifecycle/restore_archived_card; those tools onlycreate proposals and reach the same apply path covered by the API test.
Updatedrows written by earlier appliesremain in the audit log. Deliberate — the issue scopes this to forward behavior and forbids a
broad audit redesign.
Closes #2939