feat: add approval rewind and push-back history#2038
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds approval push-back rewind and history tracking: approval configs gain a rewind target, execution nodes record push-back history entries, utilities normalize/filter history, validation ensures rewind targets are upstream/reachable, runtime injects a serialized push-back payload into env, API and UI surfaces expose and render the history. Changes
Sequence DiagramsequenceDiagram
actor User
participant API as API Service
participant Store as State Store
participant Runtime as Runtime Engine
participant Executor as Step Executor
User->>API: POST push-back (inputs)
API->>API: Resolve rewind target (approval.rewind_to or step)
API->>Store: Ensure target exists & is upstream
Store-->>API: OK
API->>API: Filter inputs, build PushBackEntry (by, at, iteration, inputs)
API->>Store: Reset rewind target + dependents, set ApprovalIteration, PushBackInputs, PushBackHistory
Store-->>API: Updated
API-->>User: 200 OK / 400 on error
User->>Runtime: Execute DAG (rewound)
Runtime->>Runtime: if ApprovalIteration > 0 -> compute allowedInputs
Runtime->>Runtime: Normalize history, filter inputs
Runtime->>Runtime: Marshal DAG_PUSHBACK payload (iteration, by, at, inputs, history)
Runtime->>Executor: Inject env vars (filtered inputs + DAG_PUSHBACK)
Executor->>Executor: Step runs with injected context
Executor-->>Runtime: stdout / result
Runtime->>Store: Persist node result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (11)
internal/runtime/builtin/agentstep/convert.go (2)
125-144: Logic is correct; minor simplification possible.The nested
if len(inputs) > 0wrapper is redundant —for k := range inputsover a nil/empty map is a no-op, andmake([]string, 0, len(nil))is valid. You can flatten the block without changing behavior:♻️ Optional simplification
- keys := make([]string, 0, len(inputs)) - if len(inputs) > 0 { - var allowed map[string]struct{} - if approval != nil { - // Filter to declared input fields only (consistent with env var allowlist). - allowed = make(map[string]struct{}, len(approval.Input)) - for _, key := range approval.Input { - allowed[key] = struct{}{} - } - } - - for k := range inputs { - if allowed != nil { - if _, ok := allowed[k]; !ok { - continue - } - } - keys = append(keys, k) - } - } + var allowed map[string]struct{} + if approval != nil { + // Filter to declared input fields only (consistent with env var allowlist). + allowed = make(map[string]struct{}, len(approval.Input)) + for _, key := range approval.Input { + allowed[key] = struct{}{} + } + } + keys := make([]string, 0, len(inputs)) + for k := range inputs { + if allowed != nil { + if _, ok := allowed[k]; !ok { + continue + } + } + keys = append(keys, k) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/builtin/agentstep/convert.go` around lines 125 - 144, Remove the redundant `if len(inputs) > 0` wrapper and flatten the block so the keys slice creation and the loop over `inputs` happen unconditionally; keep the existing approval-based allowlist logic (the `allowed` map creation when `approval != nil` and the `for k := range inputs` loop that checks `allowed`) intact, referencing the `keys` variable, `inputs`, `approval`, and `allowed` so the behavior is unchanged but the extra conditional is eliminated.
119-120: Doc comment is now stale.The comment still says "filtering to only declared approval input fields", but with the new behavior the allowlist is only applied when
approval != nil. Update it to reflect the conditional filtering so future readers don't get misled.📝 Suggested wording
-// formatPushBackFeedback creates a user message from push-back inputs, -// filtering to only declared approval input fields. +// formatPushBackFeedback creates a user message from push-back inputs. +// When approval is non-nil, the inputs are filtered to its declared Input +// allowlist (consistent with the env var allowlist). When approval is nil +// (e.g. push-back originated outside an approval step), all inputs are +// included. func formatPushBackFeedback(inputs map[string]string, approval *core.ApprovalConfig) string {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/builtin/agentstep/convert.go` around lines 119 - 120, Update the doc comment for formatPushBackFeedback to reflect the new conditional behavior: state that the function creates a user message from push-back inputs and that the approval-field allowlist is applied only when the approval parameter is non-nil (otherwise inputs are passed through unfiltered); reference the function name formatPushBackFeedback and its approval parameter in the comment so readers understand the conditional filtering behavior.internal/runtime/builtin/agentstep/convert_test.go (1)
232-241: Optional: strengthen the assertion.The new behavior also emits the
Reviewer feedback:header when approval is nil but inputs are non-empty. Adding an explicit assertion would lock in that contract and guard against accidental regressions where the header is omitted but the bullets still rendered (or vice versa).🧪 Suggested strengthening
assert.Contains(t, result, "requested changes") + assert.Contains(t, result, "Reviewer feedback:") assert.Contains(t, result, "- KEY: value") + assert.Contains(t, result, "revise your response")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/builtin/agentstep/convert_test.go` around lines 232 - 241, TestFormatPushBackFeedback_NilApproval currently asserts only for "requested changes" and the bullet, but the function formatPushBackFeedback also emits a "Reviewer feedback:" header when approval is nil and inputs are non-empty; update the test to explicitly assert that the result contains "Reviewer feedback:" so the contract is locked in. Modify TestFormatPushBackFeedback_NilApproval to include an assert.Contains(t, result, "Reviewer feedback:") alongside the existing assertions, referencing the test name and formatPushBackFeedback to locate the change.api/v1/api.yaml (1)
8839-8859: TightenPushBackHistoryEntryvalidation for stronger client contracts.Line 8844 should constrain
iteration, and Line 8850 should declare date-time format since the description already says RFC3339.♻️ Proposed OpenAPI schema tightening
PushBackHistoryEntry: type: object description: "One push-back event recorded for an approval step" properties: iteration: type: integer + minimum: 1 description: "Push-back iteration number" by: type: string description: "Authenticated user who pushed the step back" at: type: string + format: date-time description: "RFC3339 timestamp when the push-back was recorded" inputs: type: object additionalProperties: type: string description: "Inputs provided for this push-back event" required: - iteration🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1/api.yaml` around lines 8839 - 8859, The PushBackHistoryEntry schema needs stronger validation: add a numeric constraint for the iteration property (e.g., add "minimum: 1" to the iteration property to enforce positive iterations) and declare the timestamp format for the at property by adding "format: date-time" so the RFC3339 description is enforced; update the PushBackHistoryEntry -> properties -> iteration and PushBackHistoryEntry -> properties -> at entries accordingly.internal/core/exec/pushback.go (1)
56-72: Consolidate duplicateclonePushBackHistoryimplementations.The exported
ClonePushBackHistoryhere is functionally equivalent to the unexportedclonePushBackHistoryininternal/runtime/transform/node.goandinternal/service/frontend/api/v1/dagruns_edit_retry.go. Consider having those callers reuseexec.ClonePushBackHistoryto avoid drift (e.g., one variant uses an explicit loop forInputs, the other usesmaps.Clone/cloneStringMap).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/exec/pushback.go` around lines 56 - 72, Duplicate implementations of clonePushBackHistory exist; consolidate by making callers reuse the exported ClonePushBackHistory in exec. Replace the unexported clonePushBackHistory functions in the other modules with calls to exec.ClonePushBackHistory (preserving behavior for Inputs via maps.Clone), remove the duplicate local functions, and update imports to reference the exec package; ensure signatures match (slice of PushBackEntry) and run tests to confirm no behavioral changes.internal/core/exec/env.go (1)
46-48: ConsiderDAG_PUSH_BACKfor consistency with other multi-word env keys.All other multi-word env keys in this file are snake_case (e.g.,
DAG_RUN_LOG_FILE,DAG_RUN_STEP_NAME,DAG_RUN_WORK_DIR).DAG_PUSHBACKcollapses the words, which is inconsistent. Since this is a user-facing env var that gets baked into DAG scripts, it'll be hard to change once released. Worth confirming the chosen name is intentional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/exec/env.go` around lines 46 - 48, The constant EnvKeyDAGPushBack currently defines the env var value as "DAG_PUSHBACK" which is inconsistent with other multi-word keys; change its value to "DAG_PUSH_BACK" in the EnvKeyDAGPushBack declaration and update any places that read or write that env var (templates, DAG scripts, tests, and any usage of EnvKeyDAGPushBack) to reference the new name; if backward compatibility is required, add code to accept the old "DAG_PUSHBACK" as a deprecated alias when reading environment variables and add/update tests and docs to reflect the new canonical name.internal/core/spec/step.go (1)
1973-1978: Consider trimming whitespace onRewindTo.Other ID/name fields are trimmed via
strings.TrimSpace(e.g.,buildStepID,buildStepName,Call). SinceRewindTois matched against step IDs/names by the validator, accidental leading/trailing whitespace in the YAML will likely fail validation with a confusing "not found" message. Consider trimming here for consistency:Proposed change
result.Approval = &core.ApprovalConfig{ Prompt: s.Approval.Prompt, Input: s.Approval.Input, Required: s.Approval.Required, - RewindTo: s.Approval.RewindTo, + RewindTo: strings.TrimSpace(s.Approval.RewindTo), }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/spec/step.go` around lines 1973 - 1978, Trim whitespace from the approval RewindTo value before assigning it to the core.ApprovalConfig to match behavior used for other ID/name fields; in the block that constructs result.Approval (where result.Approval = &core.ApprovalConfig{ Prompt: s.Approval.Prompt, Input: s.Approval.Input, Required: s.Approval.Required, RewindTo: s.Approval.RewindTo, }), replace the direct assignment of s.Approval.RewindTo with strings.TrimSpace(s.Approval.RewindTo) and ensure the strings package is imported so RewindTo comparisons by the validator won’t fail due to leading/trailing whitespace.internal/runtime/transform/node.go (1)
56-57: Prefer the sharedexec.ClonePushBackHistoryhelper over a local clone implementation.This duplicates existing logic and increases drift risk across transformation paths.
♻️ Proposed simplification
@@ - PushBackHistory: clonePushBackHistory(n.PushBackHistory), + PushBackHistory: exec.ClonePushBackHistory(n.PushBackHistory), @@ - PushBackHistory: clonePushBackHistory(node.State.PushBackHistory), + PushBackHistory: exec.ClonePushBackHistory(node.State.PushBackHistory), } } - -func clonePushBackHistory(src []exec.PushBackEntry) []exec.PushBackEntry { - if len(src) == 0 { - return nil - } - - dst := make([]exec.PushBackEntry, len(src)) - for i, entry := range src { - dst[i] = exec.PushBackEntry{ - Iteration: entry.Iteration, - By: entry.By, - At: entry.At, - } - if len(entry.Inputs) > 0 { - inputs := make(map[string]string, len(entry.Inputs)) - for key, value := range entry.Inputs { - inputs[key] = value - } - dst[i].Inputs = inputs - } - } - return dst -}Also applies to: 100-101, 104-125
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/transform/node.go` around lines 56 - 57, Replace the local clone implementation calls to clonePushBackHistory with the shared helper exec.ClonePushBackHistory to avoid duplicated logic; specifically, update the struct construction that sets PushBackHistory (and the other occurrences around the noted blocks) to call exec.ClonePushBackHistory(n.PushBackHistory) instead of clonePushBackHistory, and remove the now-unused local clonePushBackHistory function if it becomes unreferenced.internal/runtime/runner.go (1)
446-478: Recommended refactor: DRY the approval lookup and simplify the warning loop.
node.Step().Approvalis fetched twice (lines 452 and 459), and the warning loop rebuilds anallowedset that already corresponds to the keys present infilteredInputs. You can hoist the lookup once and usefilteredInputsdirectly for membership checks:♻️ Proposed simplification
if node.State().ApprovalIteration > 0 { state := node.State() env := GetEnv(ctx) + approval := node.Step().Approval var allowedInputs []string - if approval := node.Step().Approval; approval != nil { + if approval != nil { allowedInputs = approval.Input } filteredInputs := exec.FilterPushBackInputs(allowedInputs, state.PushBackInputs) for k, v := range filteredInputs { env = env.WithEnvVars(k, v) } - if approval := node.Step().Approval; approval != nil && len(filteredInputs) != len(state.PushBackInputs) { - allowed := make(map[string]struct{}, len(approval.Input)) - for _, key := range approval.Input { - allowed[key] = struct{}{} - } + if approval != nil && len(filteredInputs) != len(state.PushBackInputs) { for k := range state.PushBackInputs { - if _, ok := allowed[k]; ok { + if _, ok := filteredInputs[k]; ok { continue } logger.Warn(ctx, "Ignoring unexpected push-back input", slog.String("input", k)) } } payload, err := marshalPushBackPayload(allowedInputs, state)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/runner.go` around lines 446 - 478, Hoist the approval lookup into a single variable (e.g., approval := node.Step().Approval) and reuse it instead of calling node.Step().Approval twice; keep the existing allowedInputs derivation from approval.Input, use exec.FilterPushBackInputs(...) as before to get filteredInputs, and simplify the warning loop by checking membership against filteredInputs (or build the allowed set once from approval.Input if you prefer) when iterating state.PushBackInputs to emit logger.Warn; also reuse the hoisted approval when building the allowed map or deciding to perform the length comparison and when calling marshalPushBackPayload(allowedInputs, state) so the code no longer repeats node.Step().Approval.internal/intg/approval_test.go (1)
370-374: Hoist the repeatedindentTestScript(pushBackSnapshotScript(), 6)to a variable.Calling the same expression four times for the four steps is harmless but a bit noisy. Computing it once and reusing the result keeps the test a touch tidier.
♻️ Optional DRY tweak
-`, dagName, - indentTestScript(pushBackSnapshotScript(), 6), - indentTestScript(pushBackSnapshotScript(), 6), - indentTestScript(pushBackSnapshotScript(), 6), - indentTestScript(pushBackSnapshotScript(), 6)) +`, dagName, snippet, snippet, snippet, snippet)with
snippet := indentTestScript(pushBackSnapshotScript(), 6)declared above thefmt.Sprintfcall.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/intg/approval_test.go` around lines 370 - 374, Hoist the repeated call indentTestScript(pushBackSnapshotScript(), 6) into a single variable (e.g., snippet) declared just above the fmt.Sprintf call and use that variable in place of the four repeated expressions; this reduces duplication while leaving functions like pushBackSnapshotScript() and indentTestScript(...) unchanged and keeps the fmt.Sprintf invocation concise.ui/src/features/dags/components/common/PushBackHistory.tsx (1)
50-67: Optional: align metadata block styling with the project's slate palette.The coding guidelines call for
bg-slate-200 dark:bg-slate-700for metadata/information blocks and the documentedtext-slate-{800/600/500}hierarchy for primary/secondary/muted text. This component instead uses shadcn theme variables (bg-muted/30,text-foreground/90,text-muted-foreground). Functionally fine, but inconsistent with the convention used in similarly-purposed information blocks elsewhere in the UI.If the rest of the dags surface follows the slate-palette convention, consider switching for visual consistency in light/dark modes. If shadcn theme vars are the new direction, this is a no-op.
As per coding guidelines: "Use consistent metadata styling with
bg-slate-200 dark:bg-slate-700for similar information blocks to support both light and dark modes" and "Use text color hierarchy with primary text astext-slate-800 dark:text-slate-200, secondary astext-slate-600 dark:text-slate-400, and muted astext-slate-500 dark:text-slate-500".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/dags/components/common/PushBackHistory.tsx` around lines 50 - 67, Update the metadata block's Tailwind classes to use the project's slate palette instead of shadcn tokens: replace the container classes ("bg-muted/30", "border-border") on the outer div (the element keyed by `${entry.iteration}-${entry.at || 'pending'}` in PushBackHistory.tsx) with "bg-slate-200 dark:bg-slate-700" and keep rounded/border/padding as-is; change the primary text span (currently "text-foreground/90") to "text-slate-800 dark:text-slate-200", the secondary spans (currently "text-muted-foreground" and inner "text-foreground/80") to "text-slate-600 dark:text-slate-400" and "text-slate-500 dark:text-slate-500" respectively so the iteration/by/at hierarchy matches the documented slate-{800/600/500} scheme while preserving layout and the use of formatTimestamp(entry.at).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/src/features/dags/components/common/PushBackHistory.tsx`:
- Line 1: This file lacks the required GPL v3 two-line license header; add the
standard GPL v3 header at the top of PushBackHistory.tsx (above the import dayjs
line) to match other files (e.g., the header used in dagruns.go), or simply run
the repository helper to apply headers by executing make addlicense so the file
complies with the project's licensing guideline.
---
Nitpick comments:
In `@api/v1/api.yaml`:
- Around line 8839-8859: The PushBackHistoryEntry schema needs stronger
validation: add a numeric constraint for the iteration property (e.g., add
"minimum: 1" to the iteration property to enforce positive iterations) and
declare the timestamp format for the at property by adding "format: date-time"
so the RFC3339 description is enforced; update the PushBackHistoryEntry ->
properties -> iteration and PushBackHistoryEntry -> properties -> at entries
accordingly.
In `@internal/core/exec/env.go`:
- Around line 46-48: The constant EnvKeyDAGPushBack currently defines the env
var value as "DAG_PUSHBACK" which is inconsistent with other multi-word keys;
change its value to "DAG_PUSH_BACK" in the EnvKeyDAGPushBack declaration and
update any places that read or write that env var (templates, DAG scripts,
tests, and any usage of EnvKeyDAGPushBack) to reference the new name; if
backward compatibility is required, add code to accept the old "DAG_PUSHBACK" as
a deprecated alias when reading environment variables and add/update tests and
docs to reflect the new canonical name.
In `@internal/core/exec/pushback.go`:
- Around line 56-72: Duplicate implementations of clonePushBackHistory exist;
consolidate by making callers reuse the exported ClonePushBackHistory in exec.
Replace the unexported clonePushBackHistory functions in the other modules with
calls to exec.ClonePushBackHistory (preserving behavior for Inputs via
maps.Clone), remove the duplicate local functions, and update imports to
reference the exec package; ensure signatures match (slice of PushBackEntry) and
run tests to confirm no behavioral changes.
In `@internal/core/spec/step.go`:
- Around line 1973-1978: Trim whitespace from the approval RewindTo value before
assigning it to the core.ApprovalConfig to match behavior used for other ID/name
fields; in the block that constructs result.Approval (where result.Approval =
&core.ApprovalConfig{ Prompt: s.Approval.Prompt, Input: s.Approval.Input,
Required: s.Approval.Required, RewindTo: s.Approval.RewindTo, }), replace the
direct assignment of s.Approval.RewindTo with
strings.TrimSpace(s.Approval.RewindTo) and ensure the strings package is
imported so RewindTo comparisons by the validator won’t fail due to
leading/trailing whitespace.
In `@internal/intg/approval_test.go`:
- Around line 370-374: Hoist the repeated call
indentTestScript(pushBackSnapshotScript(), 6) into a single variable (e.g.,
snippet) declared just above the fmt.Sprintf call and use that variable in place
of the four repeated expressions; this reduces duplication while leaving
functions like pushBackSnapshotScript() and indentTestScript(...) unchanged and
keeps the fmt.Sprintf invocation concise.
In `@internal/runtime/builtin/agentstep/convert_test.go`:
- Around line 232-241: TestFormatPushBackFeedback_NilApproval currently asserts
only for "requested changes" and the bullet, but the function
formatPushBackFeedback also emits a "Reviewer feedback:" header when approval is
nil and inputs are non-empty; update the test to explicitly assert that the
result contains "Reviewer feedback:" so the contract is locked in. Modify
TestFormatPushBackFeedback_NilApproval to include an assert.Contains(t, result,
"Reviewer feedback:") alongside the existing assertions, referencing the test
name and formatPushBackFeedback to locate the change.
In `@internal/runtime/builtin/agentstep/convert.go`:
- Around line 125-144: Remove the redundant `if len(inputs) > 0` wrapper and
flatten the block so the keys slice creation and the loop over `inputs` happen
unconditionally; keep the existing approval-based allowlist logic (the `allowed`
map creation when `approval != nil` and the `for k := range inputs` loop that
checks `allowed`) intact, referencing the `keys` variable, `inputs`, `approval`,
and `allowed` so the behavior is unchanged but the extra conditional is
eliminated.
- Around line 119-120: Update the doc comment for formatPushBackFeedback to
reflect the new conditional behavior: state that the function creates a user
message from push-back inputs and that the approval-field allowlist is applied
only when the approval parameter is non-nil (otherwise inputs are passed through
unfiltered); reference the function name formatPushBackFeedback and its approval
parameter in the comment so readers understand the conditional filtering
behavior.
In `@internal/runtime/runner.go`:
- Around line 446-478: Hoist the approval lookup into a single variable (e.g.,
approval := node.Step().Approval) and reuse it instead of calling
node.Step().Approval twice; keep the existing allowedInputs derivation from
approval.Input, use exec.FilterPushBackInputs(...) as before to get
filteredInputs, and simplify the warning loop by checking membership against
filteredInputs (or build the allowed set once from approval.Input if you prefer)
when iterating state.PushBackInputs to emit logger.Warn; also reuse the hoisted
approval when building the allowed map or deciding to perform the length
comparison and when calling marshalPushBackPayload(allowedInputs, state) so the
code no longer repeats node.Step().Approval.
In `@internal/runtime/transform/node.go`:
- Around line 56-57: Replace the local clone implementation calls to
clonePushBackHistory with the shared helper exec.ClonePushBackHistory to avoid
duplicated logic; specifically, update the struct construction that sets
PushBackHistory (and the other occurrences around the noted blocks) to call
exec.ClonePushBackHistory(n.PushBackHistory) instead of clonePushBackHistory,
and remove the now-unused local clonePushBackHistory function if it becomes
unreferenced.
In `@ui/src/features/dags/components/common/PushBackHistory.tsx`:
- Around line 50-67: Update the metadata block's Tailwind classes to use the
project's slate palette instead of shadcn tokens: replace the container classes
("bg-muted/30", "border-border") on the outer div (the element keyed by
`${entry.iteration}-${entry.at || 'pending'}` in PushBackHistory.tsx) with
"bg-slate-200 dark:bg-slate-700" and keep rounded/border/padding as-is; change
the primary text span (currently "text-foreground/90") to "text-slate-800
dark:text-slate-200", the secondary spans (currently "text-muted-foreground" and
inner "text-foreground/80") to "text-slate-600 dark:text-slate-400" and
"text-slate-500 dark:text-slate-500" respectively so the iteration/by/at
hierarchy matches the documented slate-{800/600/500} scheme while preserving
layout and the use of formatTimestamp(entry.at).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 29ab550c-1a4a-4aeb-8b6b-e54cea4b9b54
📒 Files selected for processing (31)
api/v1/api.gen.goapi/v1/api.yamlinternal/cmn/schema/dag.schema.jsoninternal/core/exec/env.gointernal/core/exec/node.gointernal/core/exec/pushback.gointernal/core/spec/step.gointernal/core/step.gointernal/core/validator.gointernal/core/validator_test.gointernal/intg/approval_test.gointernal/runtime/builtin/agentstep/convert.gointernal/runtime/builtin/agentstep/convert_test.gointernal/runtime/data.gointernal/runtime/export_test.gointernal/runtime/pushback.gointernal/runtime/runner.gointernal/runtime/runner_test.gointernal/runtime/transform/node.gointernal/service/frontend/api/v1/dagruns.gointernal/service/frontend/api/v1/dagruns_edit_retry.gointernal/service/frontend/api/v1/dagruns_internal_test.gointernal/service/frontend/api/v1/transformer.gointernal/service/frontend/api/v1/transformer_test.gointernal/test/helper.goskills/dagu/references/schema.mdui/src/api/v1/schema.tsui/src/features/dags/components/approval/ApprovalTab.tsxui/src/features/dags/components/common/PushBackHistory.tsxui/src/features/dags/components/dag-details/NodeStatusTableRow.tsxui/src/features/dags/components/dag-execution/StepReviewModal.tsx
💤 Files with no reviewable changes (1)
- internal/test/helper.go
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
internal/runtime/transform/node.go (1)
55-55: Consider also cloningPushBackInputsfor consistency.
PushBackHistoryis now deep-copied, but the adjacentPushBackInputs(amap[string]string) is still assigned by reference here, while the API path ininternal/service/frontend/api/v1/dagruns.go(around lines 3379–3381) explicitly usescloneStringMap(inputs)before assignment. If the runtime↔persistence boundary is meant to fully isolate state, mirroring the samemaps.Clonetreatment forPushBackInputs(in bothToNodeandnewNode) would prevent surprises if either side later mutates the map.Note that
ApprovalInputs/OutputVariablesexhibit the same pre-existing pattern, so this is purely about matching the new cloning intent — feel free to defer.Also applies to: 99-99
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/runtime/transform/node.go` at line 55, The code currently deep-copies PushBackHistory but assigns PushBackInputs by reference; update the conversions (ToNode and newNode) to clone PushBackInputs as well (e.g., use maps.Clone or the existing cloneStringMap helper) so runtime↔persistence boundaries fully isolate state—apply the same cloning treatment where PushBackInputs is assigned, mirroring the PushBackHistory handling and ensuring consistency with the API path that calls cloneStringMap(inputs).api/v1/api.yaml (1)
7699-7701: Disallow emptyrewindTovalues.On Line 7700,
rewindToaccepts"". AddingminLength: 1tightens request validation and avoids ambiguous rewind targets.Proposed diff
rewindTo: type: string + minLength: 1 description: "Optional step name to restart from when the approver pushes the step back. Must reference the step itself or an upstream dependency."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1/api.yaml` around lines 7699 - 7701, The rewindTo schema currently allows empty strings; update the OpenAPI property named rewindTo (the string field with description "Optional step name to restart from...") to disallow empty values by adding a minLength: 1 constraint to its schema so validation rejects "" values and only accepts non-empty step identifiers.internal/service/frontend/api/v1/transformer.go (1)
354-387: Silently dropped invalid timestamps may obscure data corruption.When
time.Parse(time.RFC3339, entry.At)fails (line 381), the parse error is discarded anditems[i].Atis leftnil, which renders as a missing field in the API response. IfNormalizePushBackHistoryever emits a malformed timestamp due to a bug or storage corruption, this path will hide it from both clients and operators.Consider logging the parse error (or surfacing it as a metric) so unexpected formats become visible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/frontend/api/v1/transformer.go` around lines 354 - 387, toPushBackHistory currently swallows time.Parse errors for entry.At, hiding malformed timestamps; update this function to surface parse failures by logging the error (including entry.At and entry.Iteration and any identifying node info available) when time.Parse(time.RFC3339, entry.At) returns an error, and optionally emit a metric; keep the existing behavior of leaving items[i].At nil on parse failure but ensure the parse error is logged via the project's logger (or log.Printf if no logger is available) so malformed timestamps from NormalizePushBackHistory become visible.internal/intg/approval_test.go (1)
509-547: Consider reusingexec.PushBackEntryfor the History field and addDisallowUnknownFieldsvalidation.The
requirePushBackPayloadhelper redeclares the payload struct inline (lines 514–528). TheHistoryfield's struct definition exactly matches the exportedexec.PushBackEntrytype frominternal/core/exec/node.go, which the production code already uses. Reusingexec.PushBackEntryeliminates duplication and would catch schema drift for history items. Additionally, sincejson.Unmarshalignores unknown fields by default, the entire payload is at risk of silently accepting schema changes. Addingjson.Decoder.DisallowUnknownFields()during unmarshalling would catch any unexpected schema additions or removals in production code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/intg/approval_test.go` around lines 509 - 547, The inline payload struct in requirePushBackPayload should reuse the exported exec.PushBackEntry type for the History field and fail on unknown JSON fields: replace the anonymous History item type with []exec.PushBackEntry (to match internal/core/exec/node.go) and parse raw using a json.Decoder with DisallowUnknownFields() (e.g., create a decoder from the raw string and call decoder.DisallowUnknownFields() before Decode(&payload)); ensure any necessary imports (e.g., strings) are added. This removes duplication and causes the test to error on unexpected schema changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/runtime/runner.go`:
- Around line 446-475: The code treats approval.Input == nil the same as no
approval, allowing all push-back inputs; change the logic where allowedInputs is
set (in the block using node.Step().Approval) so that when approval != nil and
approval.Input == nil you assign allowedInputs = []string{} (an explicit empty
slice) before calling exec.FilterPushBackInputs and before passing to
marshalPushBackPayload; this will ensure exec.FilterPushBackInputs and
NormalizePushBackHistory see an explicit empty allowlist and cause unexpected
inputs to be filtered and warned about rather than passed through.
---
Nitpick comments:
In `@api/v1/api.yaml`:
- Around line 7699-7701: The rewindTo schema currently allows empty strings;
update the OpenAPI property named rewindTo (the string field with description
"Optional step name to restart from...") to disallow empty values by adding a
minLength: 1 constraint to its schema so validation rejects "" values and only
accepts non-empty step identifiers.
In `@internal/intg/approval_test.go`:
- Around line 509-547: The inline payload struct in requirePushBackPayload
should reuse the exported exec.PushBackEntry type for the History field and fail
on unknown JSON fields: replace the anonymous History item type with
[]exec.PushBackEntry (to match internal/core/exec/node.go) and parse raw using a
json.Decoder with DisallowUnknownFields() (e.g., create a decoder from the raw
string and call decoder.DisallowUnknownFields() before Decode(&payload)); ensure
any necessary imports (e.g., strings) are added. This removes duplication and
causes the test to error on unexpected schema changes.
In `@internal/runtime/transform/node.go`:
- Line 55: The code currently deep-copies PushBackHistory but assigns
PushBackInputs by reference; update the conversions (ToNode and newNode) to
clone PushBackInputs as well (e.g., use maps.Clone or the existing
cloneStringMap helper) so runtime↔persistence boundaries fully isolate
state—apply the same cloning treatment where PushBackInputs is assigned,
mirroring the PushBackHistory handling and ensuring consistency with the API
path that calls cloneStringMap(inputs).
In `@internal/service/frontend/api/v1/transformer.go`:
- Around line 354-387: toPushBackHistory currently swallows time.Parse errors
for entry.At, hiding malformed timestamps; update this function to surface parse
failures by logging the error (including entry.At and entry.Iteration and any
identifying node info available) when time.Parse(time.RFC3339, entry.At) returns
an error, and optionally emit a metric; keep the existing behavior of leaving
items[i].At nil on parse failure but ensure the parse error is logged via the
project's logger (or log.Printf if no logger is available) so malformed
timestamps from NormalizePushBackHistory become visible.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 78837f28-6163-475b-a40f-85fed7362fad
📒 Files selected for processing (11)
api/v1/api.gen.goapi/v1/api.yamlinternal/core/spec/step.gointernal/intg/approval_test.gointernal/runtime/builtin/agentstep/convert.gointernal/runtime/builtin/agentstep/convert_test.gointernal/runtime/runner.gointernal/runtime/transform/node.gointernal/service/frontend/api/v1/transformer.gointernal/service/frontend/api/v1/transformer_test.goui/src/features/dags/components/common/PushBackHistory.tsx
✅ Files skipped from review due to trivial changes (1)
- ui/src/features/dags/components/common/PushBackHistory.tsx
| // Inject push-back inputs as environment variables for any step being | ||
| // re-executed within the rewound scope. | ||
| if node.State().ApprovalIteration > 0 { | ||
| state := node.State() | ||
| env := GetEnv(ctx) | ||
| allowed := make(map[string]struct{}, len(approval.Input)) | ||
| for _, key := range approval.Input { | ||
| allowed[key] = struct{}{} | ||
| approval := node.Step().Approval | ||
| var allowedInputs []string | ||
| if approval != nil { | ||
| allowedInputs = approval.Input | ||
| } | ||
| for k, v := range node.State().PushBackInputs { | ||
| if _, ok := allowed[k]; !ok { | ||
| filteredInputs := exec.FilterPushBackInputs(allowedInputs, state.PushBackInputs) | ||
| for k, v := range filteredInputs { | ||
| env = env.WithEnvVars(k, v) | ||
| } | ||
| if approval != nil && len(filteredInputs) != len(state.PushBackInputs) { | ||
| for k := range state.PushBackInputs { | ||
| if _, ok := filteredInputs[k]; ok { | ||
| continue | ||
| } | ||
| logger.Warn(ctx, "Ignoring unexpected push-back input", slog.String("input", k)) | ||
| continue | ||
| } | ||
| env = env.WithEnvVars(k, v) | ||
| } | ||
| payload, err := marshalPushBackPayload(allowedInputs, state) | ||
| if err != nil { | ||
| logger.Warn(ctx, "Failed to marshal push-back payload", tag.Error(err)) | ||
| } else if payload != "" { | ||
| env = env.WithEnvVars(exec.EnvKeyDAGPushBack, payload) | ||
| } | ||
| ctx = WithEnv(ctx, env) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect FilterPushBackInputs and marshalPushBackPayload to confirm how each
# treats a nil vs non-nil-but-empty allowedInputs slice.
fd -t f 'pushback.go' | xargs -I{} sh -c 'echo "===== {} ====="; cat "{}"'
echo "----- marshalPushBackPayload definition -----"
ast-grep --pattern 'func marshalPushBackPayload($_, $_) ($$$) {
$$$
}'
echo "----- FilterPushBackInputs definition -----"
ast-grep --pattern 'func FilterPushBackInputs($_, $_) $_ {
$$$
}'
echo "----- ApprovalConfig.Input field -----"
rg -nP -C2 '\bInput\s+\[\]string' --type=goRepository: dagucloud/dagu
Length of output: 6719
Approval with empty input allowlist incorrectly passes all push-back inputs through.
When approval != nil but approval.Input is unset/nil, allowedInputs remains nil. FilterPushBackInputs treats len(allowed) == 0 as "no filter" (lines 14–15 in internal/core/exec/pushback.go) and returns all inputs unchanged. However, a declared approval with no input fields should filter inputs to none, not pass them all through.
This affects both FilterPushBackInputs and NormalizePushBackHistory (which calls FilterPushBackInputs internally), and the warning loop at lines 460–465 never triggers in this case since len(filteredInputs) == len(state.PushBackInputs).
Fix: When setting allowedInputs, use an explicit empty slice []string{} when approval != nil && approval.Input == nil, or pass the approval pointer downstream so callees can distinguish the two cases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/runtime/runner.go` around lines 446 - 475, The code treats
approval.Input == nil the same as no approval, allowing all push-back inputs;
change the logic where allowedInputs is set (in the block using
node.Step().Approval) so that when approval != nil and approval.Input == nil you
assign allowedInputs = []string{} (an explicit empty slice) before calling
exec.FilterPushBackInputs and before passing to marshalPushBackPayload; this
will ensure exec.FilterPushBackInputs and NormalizePushBackHistory see an
explicit empty allowlist and cause unexpected inputs to be filtered and warned
about rather than passed through.
There was a problem hiding this comment.
Keeping the current behavior intentionally. For this feature, omitted approval.input means there is no allowlist, so push-back inputs are preserved as-is; only a populated input list filters keys. Treating approval.Input == nil as an explicit empty allowlist would break the no-input-defined flow that this branch implements and documents.
There was a problem hiding this comment.
@yottahmd, understood — thanks for the clarification. The semantics are clear: an absent approval.input means "pass all push-back inputs through," while a populated list acts as an explicit allowlist. I'll note this for future reviews.
✏️ Learnings added
Learnt from: yottahmd
Repo: dagucloud/dagu PR: 2038
File: internal/runtime/runner.go:446-475
Timestamp: 2026-04-26T02:59:34.541Z
Learning: In `internal/runtime/runner.go` and `internal/core/exec/pushback.go` (dagucloud/dagu), when `approval.Input` is nil/unset, `FilterPushBackInputs` intentionally treats it as "no allowlist" and passes all push-back inputs through unchanged. Only a non-nil, populated `approval.Input` slice acts as an explicit allowlist. Flagging nil as an empty allowlist would break the intended no-input-defined flow.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Summary
Testing
Notes
Summary by CodeRabbit
New Features
Validation / Error handling
Documentation