Skip to content

fix(go-agent): support task-based run cancellation#17188

Draft
Hz-186 wants to merge 1 commit into
infiniflow:mainfrom
Hz-186:fix/agent-pause-red-window
Draft

fix(go-agent): support task-based run cancellation#17188
Hz-186 wants to merge 1 commit into
infiniflow:mainfrom
Hz-186:fix/agent-pause-red-window

Conversation

@Hz-186

@Hz-186 Hz-186 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add the task cancellation endpoint used by the agent stop action
  • cancel active agent runs by emitted task ID

Testing

  • bash build.sh --test ./internal/agent/canvas ./internal/router

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 21, 2026
@Hz-186
Hz-186 marked this pull request as draft July 21, 2026 10:28
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds task-scoped cancellation by tracking active task IDs in the runner, authorizing cancellation against the task’s canvas, and exposing an authenticated POST endpoint.

Changes

Task cancellation flow

Layer / File(s) Summary
Runner task tracking and cancellation
internal/agent/canvas/runner.go
Runner tracks task cancellation channels and canvas ownership, removes mappings after completion, and exposes task lookup and cancellation methods.
Authorized cancellation endpoint
internal/service/agent.go, internal/handler/agent.go, internal/router/router.go
The service verifies canvas access before cancellation, and the authenticated handler and router expose POST /api/v1/tasks/:task_id/cancel.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AgentHandler
  participant AgentService
  participant Runner
  Client->>AgentHandler: POST /api/v1/tasks/:task_id/cancel
  AgentHandler->>AgentService: CancelTask(userID, taskID)
  AgentService->>Runner: TaskCanvas(taskID)
  AgentService->>AgentService: loadCanvasForUser(userID, canvasID)
  AgentService->>Runner: CancelTask(taskID)
  Runner-->>AgentHandler: cancellation result
  AgentHandler-->>Client: success response
Loading

Suggested reviewers: qinling0210

Poem

I’m a rabbit with a task in flight,
Mapping canvas paths just right.
A cancel route hops through the gate,
Checks its owner, then stops its fate.
Cleanup nibbles stale trails away—
“Thump!” says the run, and ends its day.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: task-based cancellation support for agent runs.
Description check ✅ Passed The description includes the required Summary section and a useful Testing section that matches the PR objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/service/agent.go`:
- Around line 1888-1901: Prevent the cancellation TOCTOU race by passing the
authorized canvasID from AgentService.CancelTask to runner.CancelTask in
internal/service/agent.go lines 1888-1901. Update runner.CancelTask in
internal/agent/canvas/runner.go lines 387-403 to accept expectedCanvasID and,
while holding the runner lock, verify the task still belongs to that canvas
before cancelling; otherwise leave it unchanged.
🪄 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: 82530c1f-1e47-4889-a9e5-313325a46324

📥 Commits

Reviewing files that changed from the base of the PR and between edeb47c and fbf7bb4.

📒 Files selected for processing (4)
  • internal/agent/canvas/runner.go
  • internal/handler/agent.go
  • internal/router/router.go
  • internal/service/agent.go

Comment thread internal/service/agent.go
Comment on lines +1888 to +1901
func (s *AgentService) CancelTask(ctx context.Context, userID, taskID string) error {
if taskID == "" || s.runner == nil {
return nil
}
canvasID, active := s.runner.TaskCanvas(taskID)
if !active {
return nil
}
if _, err := s.loadCanvasForUser(ctx, userID, canvasID); err != nil {
return err
}
s.runner.CancelTask(taskID)
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Prevent TOCTOU race condition during task cancellation.

There is a Time-Of-Check to Time-Of-Use (TOCTOU) race condition between s.runner.TaskCanvas(taskID) and s.runner.CancelTask(taskID). If an attacker quickly reassigns a taskID (e.g., via a colliding version_id) to a victim's canvas in the short window after the authorization check but before the actual cancellation, they could bypass authorization and cancel the victim's task. Fix this by passing the authorized canvas ID to the runner and atomically validating it inside the runner's lock.

  • internal/service/agent.go#L1888-L1901: pass the authorized canvasID to s.runner.CancelTask to ensure we only cancel the verified canvas.
  • internal/agent/canvas/runner.go#L387-L403: update CancelTask to accept and enforce expectedCanvasID while holding the lock.
🔒️ Proposed fix to enforce canvas ownership during cancellation

In internal/service/agent.go:

 	if _, err := s.loadCanvasForUser(ctx, userID, canvasID); err != nil {
 		return err
 	}
-	s.runner.CancelTask(taskID)
+	s.runner.CancelTask(taskID, canvasID)
 	return nil

In internal/agent/canvas/runner.go:

-// CancelTask signals an active run identified by the task_id emitted in its
-// events. It returns the owning canvas id when the task is still active.
-func (r *Runner) CancelTask(taskID string) (string, bool) {
+// CancelTask signals an active run by task_id, ensuring it belongs to the expected canvas.
+func (r *Runner) CancelTask(taskID, expectedCanvasID string) bool {
 	r.mu.Lock()
 	cancel, ok := r.taskCancels[taskID]
 	canvasID := r.taskCanvases[taskID]
 	r.mu.Unlock()
-	if !ok {
-		return "", false
+	if !ok || canvasID != expectedCanvasID {
+		return false
 	}
 	select {
 	case <-cancel:
 	default:
 		close(cancel)
 	}
-	return canvasID, true
+	return true
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *AgentService) CancelTask(ctx context.Context, userID, taskID string) error {
if taskID == "" || s.runner == nil {
return nil
}
canvasID, active := s.runner.TaskCanvas(taskID)
if !active {
return nil
}
if _, err := s.loadCanvasForUser(ctx, userID, canvasID); err != nil {
return err
}
s.runner.CancelTask(taskID)
return nil
}
func (s *AgentService) CancelTask(ctx context.Context, userID, taskID string) error {
if taskID == "" || s.runner == nil {
return nil
}
canvasID, active := s.runner.TaskCanvas(taskID)
if !active {
return nil
}
if _, err := s.loadCanvasForUser(ctx, userID, canvasID); err != nil {
return err
}
s.runner.CancelTask(taskID, canvasID)
return nil
}
Suggested change
func (s *AgentService) CancelTask(ctx context.Context, userID, taskID string) error {
if taskID == "" || s.runner == nil {
return nil
}
canvasID, active := s.runner.TaskCanvas(taskID)
if !active {
return nil
}
if _, err := s.loadCanvasForUser(ctx, userID, canvasID); err != nil {
return err
}
s.runner.CancelTask(taskID)
return nil
}
// CancelTask signals an active run by task_id, ensuring it belongs to the expected canvas.
func (r *Runner) CancelTask(taskID, expectedCanvasID string) bool {
r.mu.Lock()
cancel, ok := r.taskCancels[taskID]
canvasID := r.taskCanvases[taskID]
r.mu.Unlock()
if !ok || canvasID != expectedCanvasID {
return false
}
select {
case <-cancel:
default:
close(cancel)
}
return true
}
📍 Affects 2 files
  • internal/service/agent.go#L1888-L1901 (this comment)
  • internal/agent/canvas/runner.go#L387-L403
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/agent.go` around lines 1888 - 1901, Prevent the cancellation
TOCTOU race by passing the authorized canvasID from AgentService.CancelTask to
runner.CancelTask in internal/service/agent.go lines 1888-1901. Update
runner.CancelTask in internal/agent/canvas/runner.go lines 387-403 to accept
expectedCanvasID and, while holding the runner lock, verify the task still
belongs to that canvas before cancelling; otherwise leave it unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant