Skip to content

Add Aurora Actions — user-defined background automations - #366

Merged
damianloch merged 45 commits into
mainfrom
feat/actions-system
May 7, 2026
Merged

Add Aurora Actions — user-defined background automations#366
damianloch merged 45 commits into
mainfrom
feat/actions-system

Conversation

@damianloch

@damianloch damianloch commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new "Actions" feature that lets users define reusable background automations in natural language. Users write instructions, pick a trigger type (manual, on incident completion, or on a recurring schedule), and Aurora's agent executes them as background tasks using all connected integrations.

The core use case is automating repetitive SRE workflows — things like adjusting noisy alert thresholds via Terraform PRs, running post-incident health checks, or kicking off infrastructure audits on a schedule.

What's included

Backend

  • CRUD API for actions and action runs with full RBAC
  • Executor service that dispatches actions as background chat sessions with proper RLS context
  • Celery Beat scheduler for interval-based triggers
  • Stale run cleanup integrated into existing background chat janitor
  • Dedicated action prompt scaffolding (full tool access, eager-loaded skills, no RCA mandates)

Frontend

  • Actions management page embedded in Settings modal
  • Create, edit, enable/disable, trigger, and view run history
  • "Run Action" dropdown on completed incidents
  • /action slash command in chat with autocomplete, inline styling, and tool-call integration
  • Real-time polling for active run status updates

Chat integration

  • /action <name> triggers via LLM tool call (same pattern as RCA trigger)
  • Agent processes the action dispatch then continues responding to the rest of the user's message
  • Internal scaffold messages hidden from chat UI at the persistence layer

Test plan

  • Create action via Settings > Actions, verify it appears in list
  • Trigger action manually, confirm run appears and completes
  • Edit action instructions, verify update persists
  • Use /action command in chat, verify tool call renders with correct icon
  • Configure scheduled action, verify it dispatches after interval
  • Trigger from incident card, confirm incident context flows into prompt
  • Restart containers, verify stale runs get cleaned up

Summary by CodeRabbit

  • New Features
    • Full Actions management UI: create, edit, enable/disable, delete, run now, and view run history/status.
    • Actions integrated into chat: select/trigger actions from the input, slash-command menu, and enhanced send behavior.
    • Incident cards: “Run Action” dropdown for incident-triggered actions.
    • Settings: new Actions tab.
    • Scheduled execution: automatic dispatch of on-schedule actions and visible scheduled run tracking.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@damianloch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 19 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c391631b-9a5c-433f-9aff-1eaa753f5dc3

📥 Commits

Reviewing files that changed from the base of the PR and between 3c2e333 and a0b8029.

📒 Files selected for processing (3)
  • server/chat/background/task.py
  • server/main_chatbot.py
  • server/routes/actions/actions_routes.py

Walkthrough

Introduces a comprehensive Actions management system enabling users to create, manage, and trigger background automations. Includes a full-page Actions UI with CRUD and detail views, chat-based action selection via slash commands, incident card action triggering, Celery scheduler for periodic execution, agent middleware to route action requests, and new API routes backed by executor and database schema.

Changes

Actions Management System

Layer / File(s) Summary
Database Schema
server/utils/db/db_utils.py
New actions and action_runs tables with RLS support; indexes on org, trigger type, and run status.
Backend API Routes
server/routes/actions/*
CRUD endpoints (GET /api/actions, POST, GET by id, PUT, DELETE), trigger endpoint (POST /{id}/run), and run history listing (GET /{id}/runs); includes validation, error handling, and rate limiting.
Action Executor
server/services/actions/executor.py
Core dispatch logic: loads action, enforces rate limits, loads incident context, builds prompt, creates run record, starts background chat session, and enqueues async execution; includes run status updates and prompt building helpers.
Scheduler
server/services/actions/scheduler.py, server/celery_config.py
Periodic Celery task to discover and dispatch on_schedule actions every 60 seconds; includes interval checking and dispatch error handling.
Agent Middleware & Prompts
server/chat/backend/agent/agent.py, server/chat/backend/agent/prompt/background.py, server/chat/backend/agent/prompt/composer.py, server/chat/backend/agent/prompt/prompt_builder.py
Adds trigger_action_id detection and ForceToolChoice middleware; builds action-mode prompt segment with skill loading; adjusts system invariant and segment ordering for action vs. RCA background modes.
Agent Tool
server/chat/backend/agent/tools/trigger_action_tool.py, server/chat/backend/agent/tools/cloud_tools.py
New LLM-callable trigger_action tool with args validation; conditionally registered when action is requested; includes error handling and dispatch integration.
State & Workflow
server/chat/backend/agent/utils/state.py, server/chat/backend/agent/workflow.py, server/main_chatbot.py
Adds trigger_action_id field to State; propagates action ID from incoming messages through workflow; gates RCA scaffold message persistence and UI exposure; detects action-triggered background runs.
Background Task Integration
server/chat/background/task.py
Extends background chat lifecycle to handle action-triggered runs: marks action runs as success or error based on chat completion status, timeout, or failure; includes cleanup for stale action runs; tags RCA scaffold messages.
Authorization
server/utils/auth/enforcer.py
Adds action resource with read (viewer) and write (editor) permissions; introduces domain-aware policy seeding with Casbin domain-matching function.
API Registration
server/main_compute.py
Registers actions blueprint at /api/actions.
Client API Routes
client/src/app/api/actions/*
Next.js proxy routes that forward GET/POST/PUT/DELETE requests to backend; includes action detail, update, delete, trigger, and run history endpoints.
Client UI: Pages & Views
client/src/app/actions/page.tsx
Full Actions management page with list view (stats, table), detail view (status, config, history), and form view (create/edit); includes data fetching hooks, UI components (StatCard, Panel, badges), and view orchestration.
Client UI: Incident Integration
client/src/app/incidents/components/IncidentCard.tsx
Adds RunActionDropdown component to incident header when complete; fetches enabled actions, triggers via API with incident context, displays results with toast notifications.
Client UI: Chat Integration
client/src/app/chat/components/ChatClient.tsx, client/src/app/chat/components/useChatSendHandlers.ts
Fetches available actions; extends send handlers with action selection, action parsing, and session management; propagates trigger_action, attachments, and ui_state in chat payloads; clears selection after send.
Client UI: Slash Commands
client/src/components/chat/SlashCommandMenu.tsx, client/src/components/chat/enhanced-chat-input.tsx
New SlashCommandMenu component rendering action list filtered by input; exports getFilteredActions helper; integrates into EnhancedChatInput with keyboard navigation, action selection overlay, and Send button gating based on selected action.
Settings Modal
client/src/components/SettingsModal.tsx
Adds "Actions" tab with Workflow icon; renders ActionsContent when selected.
Tool Logo
client/src/components/tool-calls/CommandLogo.tsx
Adds triggerAction logo SVG and mapping for tool === 'trigger_action'.
Query System
client/src/lib/query.ts
Adds optional refreshInterval option to enable polling; hook polls at specified interval (ms) if enabled.

Sequence Diagram

sequenceDiagram
    actor User
    participant Chat as Chat UI
    participant Client as Client App
    participant Backend as Backend API
    participant Agent as Agent
    participant Executor as Action Executor
    participant BG as Background Chat
    participant DB as Database

    User->>Chat: Type "/action"
    Chat->>Chat: Show SlashCommandMenu with actions
    User->>Chat: Select action
    Chat->>Client: onActionSelect(action)
    Client->>Client: Store selectedAction in state
    
    User->>Chat: Send message with action
    Chat->>Backend: POST /chat/send {trigger_action, message}
    Backend->>Agent: Process with trigger_action_id
    Agent->>Agent: ForceToolChoice("trigger_action")
    Agent->>Agent: trigger_action tool decides
    Agent->>Backend: Invoke trigger_action(action_id)
    
    Backend->>Executor: dispatch_action(action_id)
    Executor->>DB: Load action config
    Executor->>DB: Create action_run (pending)
    Executor->>BG: Start background chat session
    Executor->>DB: Update action_run (running)
    
    BG->>BG: Execute action with context
    
    alt Success
        BG->>DB: Chat completes
        Executor->>DB: Update action_run (success)
    else Timeout
        BG->>DB: Timeout after 30min
        Executor->>DB: Update action_run (error)
    else Failure
        BG->>DB: Chat fails
        Executor->>DB: Update action_run (error)
    end
    
    DB->>User: Action run visible in history
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Arvo-AI/aurora#276: Modifies prompt composer logic; related to prompt segment ordering changes.
  • Arvo-AI/aurora#344: Touches background cleanup logic (cleanup_stale_background_chats); related to stale run handling.
  • Arvo-AI/aurora#268: Related changes to background chat/session initialization and RCA scaffold handling.

Suggested reviewers

  • Zarlanx
  • beng360
  • OlivierTrudeau

Poem

🐰 I hopped along the code-lined way,

Planted Actions where runs now play,
Slash commands guide a gentle start,
Background chats perform their part,
Hooray — the rabbits stitched this art.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main feature being introduced: Aurora Actions as a user-defined background automation capability.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/actions-system

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/chat/background/task.py Fixed
Comment thread server/chat/background/task.py Fixed
Comment thread server/chat/background/task.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread client/src/components/chat/enhanced-chat-input.tsx Fixed
Comment thread client/src/app/chat/components/ChatClient.tsx Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/services/actions/executor.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
@damianloch
damianloch marked this pull request as ready for review May 5, 2026 19:44
@damianloch
damianloch requested a review from a team as a code owner May 5, 2026 19:44
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
Comment thread server/routes/actions/actions_routes.py Fixed
@damianloch damianloch changed the title Feat/actions system Add Aurora Actions — user-defined background automations May 5, 2026
@sonarqubecloud

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

damianloch added 14 commits May 5, 2026 17:12
Adds a general-purpose Actions feature where users define natural language
instructions that Aurora executes as background agent tasks using connected
tools (IaC, GitHub, Datadog, etc.). Replaces the need for hardcoded
automation pipelines.

Backend:
- actions + action_runs DB tables with RLS
- CRUD + trigger routes at /api/actions
- Executor service (dispatch_action, build_action_prompt)
- Completion hooks in run_background_chat (all exit paths + finally)
- Dedicated RBAC resource for actions (viewer read, editor write)

Frontend:
- Full actions page with list/detail/create views
- API proxy routes
- Run Action dropdown on IncidentCard (post-RCA trigger)
- Sidebar nav entry
- Refactor CreateActionView into ActionFormView supporting create + edit
- Add Edit button in action detail view header
- Add on_schedule trigger type with interval picker (min 5 minutes)
- Add scheduler Celery beat task (runs every 60s, dispatches due actions)
Adds a 4th step to cleanup_stale_background_chats that marks any
action_runs stuck in running/pending for >20 minutes as error.
Handles container restarts where the finally block never executed.
dispatch_action, _create_run, and _update_run now call set_rls_context
so they work when invoked from the scheduler (no Flask request context).
Stale action runs cleanup iterates per-org to respect RLS policies.
- Extract _get_action_response() helper to avoid calling decorated
  get_action() from update_action() (TypeError: too many args)
- Replace datetime.utcnow() with datetime.now(timezone.utc) in both
  routes and executor
Users can type /action <name> in chat to trigger an Aurora Action
without leaving the conversation. Includes autocomplete dropdown,
comprehensive error handling (404, 403, 429, network errors),
and graceful empty-state messaging.
- Arrow Up/Down navigates the autocomplete dropdown
- Enter/Tab selects highlighted action
- Selected action renders as a purple chip (like incident context)
- Input clears after selection so user can type additional context
- Submit uses stored action ID directly (no fragile text re-parsing)
- Text-based /action fallback still works for power users
- Remove action instruction prepend from user messages to prevent
  system prompt leaking into chat UI (action_id passed via tool desc)
- Fix timestamps showing "just now" by appending Z suffix to isoformat
- Use formatTimeAgo for consistent relative time display
- Fix actions.filter crash when shared query cache returns object shape
- Add trigger_action icon matching Settings modal Workflow icon
- Replace backtrack-prone regex patterns (\s+ followed by .+/.*?) with
  single \s followed by .* to eliminate super-linear runtime risk
- Remove dead conditional that returned same value for both branches
Replace regex-based parsing with string operations (startsWith, search
+ slice) to fully avoid patterns Sonar flags as backtrack-vulnerable.
- Remove user-controlled data from log messages (S5145 log injection)
- Replace empty except blocks with debug logging
- Remove unused _VALID_MODES constant and unused user_id param
- Remove unused fetchR import
- Replace regex with string operations to avoid backtracking (S5852)
damianloch added 7 commits May 5, 2026 17:12
…reated

- Add dispatch_on_incident_actions() that finds enabled on_incident
  actions and dispatches each with full incident context
- Hook into run_background_chat at RCA start (after incident linking)
  with source != 'action' guard to prevent infinite loops
- Add duration_ms to list_runs endpoint (was missing)
@damianloch
damianloch force-pushed the feat/actions-system branch from d794472 to 6ca2580 Compare May 5, 2026 21:12

@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: 28

🤖 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 `@client/src/app/actions/page.tsx`:
- Around line 439-442: getIntervalSeconds() is incorrectly clamping intervals to
300 seconds which hides invalid <5min inputs; change getIntervalSeconds to
return the raw computed seconds (Math.round(intervalValue *
multipliers[intervalUnit])) without Math.max so validation can detect and
disable submit for intervals <300. Also remove or adjust the same clamping logic
in the other duplicated spots referenced in this diff so the UI and submission
use the true entered value and the existing validation path can surface the
error instead of silently rewriting to 300.
- Around line 196-200: The table rows rendered in actions.map (keyed by
action.id) are only clickable with the mouse and not keyboard accessible; make
each row focusable and activatable by adding tabIndex={0}, a semantic role
(e.g., role="button"), and an onKeyDown handler that calls onSelect(action) when
Enter or Space is pressed (mirror the existing onClick behavior). Also ensure
any necessary ARIA attributes (e.g., aria-label or aria-labelledby) are present
so screen readers can announce the row; update the <tr> where onClick={() =>
onSelect(action)} is used to include these changes.

In `@client/src/app/chat/components/useChatSendHandlers.ts`:
- Around line 203-233: When a chip action (selectedAction/actionToTrigger) is
used with an empty draft, the code still uses trimmed for the visible message,
ensureSession title and socket.query causing empty bubbles/titles/payloads;
update useChatSendHandlers so that when actionToTrigger exists but trimmed is
empty, substitute a non-empty string (e.g., actionToTrigger.name) for the
user-visible text, the title passed to ensureSession, and the query in
socket.send; ensure onNewMessage(...) receives that substituted text,
setIsSending and clearSelectedAction behavior remains the same, and retain
checks for socket.isReady and actionToTrigger.id.
- Around line 161-171: parseActionFromText only matches when the slash command
is at column 0; update it to find a "/action " or "/actions " token anywhere in
the text (start or after whitespace) and extract the action name that follows so
commands still work when ChatClient prepends incident context or other leading
text. Locate the parseActionFromText function and replace the startsWith logic
with a search (or regex) that finds the last occurrence of "/action " or
"/actions " (matching case-insensitively and ensuring it's either at start or
preceded by whitespace), extract the trailing name token, trim it, then lookup
availableActions by name (case-insensitive) and if not found return { id: '',
name } as before. Ensure the dependency on availableActions remains.

In `@client/src/app/incidents/components/IncidentCard.tsx`:
- Around line 117-165: The dropdown trigger button (handled by handleOpen) needs
ARIA attributes and the outside-click effect (useEffect) must also handle Escape
so keyboard users can dismiss the menu: add aria-haspopup="menu" and
aria-expanded={open} (and aria-label if needed) to the button element, and
update the useEffect that registers the outside-click listener (the effect
referencing ref and open) to also attach a keydown listener that closes the menu
on Escape (ensure both listeners are removed in the cleanup). Use the existing
ref/open state and the handleOpen/handleTrigger flow when implementing these
changes so behavior and cleanup stay consistent.
- Around line 126-140: handleOpen silently ignores non-2xx responses and uses
raw fetch instead of the project's fetchR; replace the raw fetch call in
handleOpen with the imported fetchR (matching its actual generic/response shape)
so HTTP errors are normalized, ensure non-OK responses trigger a toast (use the
response/error returned by fetchR), and move setLoading(false) into a finally
block so loading state is always cleared; update the setActions line to handle
the fetchR response shape and keep setOpen(true)/setOpen(false) behavior intact.

In `@client/src/components/chat/enhanced-chat-input.tsx`:
- Around line 110-134: The Escape key handler in handleMenuKey should
dismiss/reset the slash UI instead of wiping the entire draft; replace the
setInput('') call with logic that closes the menu and resets menu state (e.g.,
call setMenuVisible(false), setMenuStage(undefined or ''), and
setHighlightedIndex(0)), and if needed remove only the active slash token from
the current input by updating setInput(prev => prev without the slash token)
rather than clearing all text; update the Escape branch in handleMenuKey to use
setMenuVisible, setMenuStage, setHighlightedIndex and optionally a targeted
setInput transformation instead of setInput('').
- Around line 240-249: The onChange handler for AutoResizeTextarea must clear
stale selectedAction when the draft no longer matches it: inside the onChange
used in enhanced-chat-input (the handler that currently calls setInput and
setHighlightedIndex), parse the new text for the action token (e.g., leading
"/action <name>" or whatever token format your app uses) and if no token is
present or the token name differs from selectedAction?.name call
setSelectedAction(null); implement the parsing inline or via a small helper
(e.g., parseActionFromText) and apply the same check in onPaste if you have one
so the selectedAction cannot be dispatched when the draft diverges.

In `@client/src/lib/query.ts`:
- Around line 309-315: The polling useEffect (watching active and
refreshInterval) currently runs regardless of tab visibility; modify it so the
interval only runs while document.visibilityState === 'visible': inside the
useEffect check visibility before creating the setInterval (and if hidden, do
not start it), add a 'visibilitychange' listener that clears the interval when
document becomes hidden and restarts it (and triggers an immediate
queryClient.fetch(active, fetcherRef.current, optsRef.current)) when visibility
becomes visible, and ensure the listener and interval are both cleaned up in the
effect return; update references to active, refreshInterval, queryClient.fetch,
fetcherRef, and optsRef accordingly.
- Around line 308-315: The polling useEffect uses queryClient.fetch which is
subject to staleTime and so may not trigger network requests as expected; change
the interval callback to call queryClient.invalidate(active) (or
queryClient.invalidateQueries(active)) instead of queryClient.fetch(active,
fetcherRef.current, optsRef.current) so each tick forces a revalidate regardless
of staleTime; keep the same guard (if (!active || !refreshInterval) return) and
clearInterval behavior, and ensure you're using the same active and
refreshInterval variables from this effect.

In `@server/chat/backend/agent/agent.py`:
- Around line 515-518: Detect and guard against both state.trigger_rca_requested
and state.trigger_action_id being set at the same time before modifying
middlewares: check if getattr(state, "trigger_rca_requested", False) and
getattr(state, "trigger_action_id", None) are both truthy and either raise a
clear exception (e.g., ValueError/RuntimeError) or enforce a single,
well-documented precedence rule; implement this check right before the existing
middlewares.insert(0, _ForceToolChoice(...)) calls so conflicting flags are
handled deterministically and fail fast instead of silently letting the second
insert override the first.

In `@server/chat/backend/agent/prompt/background.py`:
- Around line 165-197: build_action_mode_segment currently silently skips
attempting to load skills when rca_context.user_id is missing; mirror
build_background_mode_segment by adding a warning log when user_id is falsy so
the silent skip is observable in production—inside build_action_mode_segment,
after computing user_id (and before the try/import of SkillRegistry), emit
logger.warning (or logger.info per project convention) indicating that user_id
is missing and skills will not be loaded for action mode, referencing
rca_context and/or providers to aid debugging.

In `@server/chat/backend/agent/prompt/composer.py`:
- Around line 184-188: The code is detecting RCA background mode by searching
for the literal header string in the rendered prompt (is_rca_background =
segments.background_mode and "BACKGROUND RCA MODE" in segments.background_mode);
instead, add an explicit boolean flag (e.g., is_rca_background) to the
PromptSegments data structure, set that flag where the background segment is
built (in build_background_mode_segment or the PromptSegments constructor), and
then change the checks to use segments.is_rca_background when deciding to append
segments.terraform_validation and segments.failure_recovery so prompt behavior
no longer depends on rendered text.

In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1469-1478: The trigger_action tool currently trusts the
model-supplied action_id; wrap or replace final_func passed to
StructuredTool.from_function so it enforces the preselected ID (compare against
state_context.trigger_action_id or always use that value) instead of using the
model argument _action_id: modify the code that constructs final_func (or wrap
final_func) to validate equality and return an error when they differ, or to
ignore the incoming action_id and call the real action with
state_context.trigger_action_id, then pass that wrapped/validated function into
StructuredTool.from_function(name='trigger_action',
args_schema=TriggerActionArgs).

In `@server/chat/backend/agent/tools/trigger_action_tool.py`:
- Around line 16-24: The tool must treat the server-selected action ID as
authoritative: in trigger_action (and TriggerActionArgs handling) first resolve
the canonical action id from injected server/conversation context (the
selected_action_id stored in request/session state) and use that as the
effective id; if the model-supplied action_id argument is present and does not
match the canonical id, reject the call (return an error or raise an exception)
and log the mismatch instead of executing the model-provided id; update
callers/loading of TriggerActionArgs to obtain the canonical id from context
rather than relying on the model input.

In `@server/chat/background/task.py`:
- Around line 2318-2329: The stale-action janitor in task.py is marking
running/pending action_runs as error too early; update the cleanup query used in
the loop (the UPDATE on action_runs near stale_threshold and the logic around
stale_actions_count/run_background_chat) so it does not flip long-running jobs
that may still be active: either extend the threshold to the run_background_chat
max (30 minutes) or, better, add exclusions to the WHERE clause to skip
action_runs that have an associated live chat or active Celery task (e.g., check
for non-null chat_session_id that exists in chat_sessions and/or a non-completed
celery_task_id or an “active” flag), ensuring you reference the same columns
used in action_runs (chat_session_id, celery_task_id) and the stale_threshold
variable rather than blindly updating all started_at < stale_threshold.
- Around line 1111-1116: The code marks every background chat opener as an RCA
scaffold by unconditionally adding additional_kwargs={"is_rca_scaffold": True}
to the HumanMessage; change this so is_rca_scaffold is only set when the opener
is actually a synthesized/internal RCA scaffold (e.g., check the opener/source
flag used by Workflow or the background session type) — locate the HumanMessage
construction (human_message = HumanMessage(...)) and wrap the additional_kwargs
assignment in a condition that checks for synthesized/internal RCA (or only set
additional_kwargs when a boolean like is_synthesized_rca or opener_type == "rca"
is true) so user-authored prompts for Slack/Google Chat/manual sessions are not
skipped by Workflow.

In `@server/routes/actions/actions_routes.py`:
- Around line 142-166: The list_actions handler (and similarly update_action and
list_runs) currently performs DB queries without try/except; wrap the DB access
and row processing in a try/except block that mirrors
delete_action/trigger_action: catch Exception as e, log the exception (using the
same logger used elsewhere), and return a structured JSON error response (e.g.,
jsonify({"error": "Failed to list actions", "details": str(e)}) with a 500
status) so DB failures do not propagate as unstructured 500s; ensure you
reference the existing symbols list_actions, update_action, and list_runs and
keep the DB pool usage (db_pool.get_connection(), conn.cursor(), and subsequent
row processing) inside the try.
- Around line 325-326: The request handler trigger_action currently copies
body["incident_id"] into trigger_context and passes it to dispatch_action
without validation; update trigger_action to run the same _validate_uuid check
used for action_id (call _validate_uuid(body.get("incident_id"))) before
assigning to trigger_context and only add incident_id if validation passes
(otherwise raise/return the same error handling used for invalid action_id),
ensuring dispatch_action always receives a validated UUID.
- Around line 144-155: The list_actions query is missing tenant filtering and
returns actions across all organizations; modify the SQL in list_actions to add
a WHERE clause (e.g., "WHERE a.org_id = %s") and pass the resolved org_id for
the requesting user (same pattern used in create_action) so only actions for
that org are returned; ensure you add the parameter to cur.execute and preserve
the existing GROUP BY/ORDER BY logic.
- Around line 191-206: The code in create_action after
db_pool.get_connection()/conn.cursor() fetches org_id from "SELECT org_id FROM
users WHERE id = %s" and sets org_id = None when no row is returned, which later
causes an IntegrityError on INSERT; add an explicit guard after fetching org_id
(check the org_id variable or whether row is None) and handle the missing
user/org case by aborting before the INSERT (e.g., return a 400/404 or raise a
specific application error) so the INSERT into actions (which requires org_id)
is never attempted; update create_action to log the missing user id and return a
clear client error instead of letting the IntegrityError propagate.
- Around line 228-266: Resolve the caller's org_id by selecting "org_id" from
the "users" row for the authenticated user (same pattern used in create_action),
then enforce it on every single-resource query: in _get_action_response,
update_action, delete_action and list_runs add "AND org_id = %s" to the
SELECT/UPDATE/DELETE statements (and to the action_runs SELECT) and pass the
resolved org_id as a parameter alongside action_id; ensure you use the same
unique symbols (function names _get_action_response, update_action,
delete_action, list_runs) so every database call that currently filters only by
id also includes the org_id guard.

In `@server/services/actions/executor.py`:
- Around line 79-83: Pass the originating incident_id through when creating the
background chat session and when enqueuing the background task so
State.incident_id is set; specifically, update the
create_background_chat_session call to include incident_id=incident_id (or the
local incident variable) and ensure the run_background_chat enqueue call
includes incident_id=incident_id as an argument so the session↔incident linkage
and downstream State.incident_id are preserved.
- Around line 87-100: The enqueue failure handler must also clean up the
pre-created chat session (session_id) so it doesn't remain stuck in
"in_progress": in the except block that catches exceptions from
run_background_chat.delay, call the chat-session cleanup routine (for example
update_chat_session_status(session_id, status="failed") or
delete_chat_session(session_id) — whichever existing function is used elsewhere)
before or alongside calling _update_run(run_id, ...), and wrap that cleanup call
in its own try/except to avoid masking the original error; ensure you reference
session_id and run_id and log any cleanup errors but still re-raise the original
exception.
- Around line 111-132: The guardrail text variable rail_text only contains
action["instructions"] but must also include the untrusted trigger data returned
by _format_trigger_context(trigger_context); update the code that builds
rail_text (and the value returned alongside the full prompt in the return
statement) so it concatenates action["instructions"] with the formatted ctx
(even if empty include an explicit context section), e.g. append ctx under a
"Context" heading produced by _format_trigger_context(trigger_context), ensuring
the same ctx used in parts is included in rail_text so external trigger fields
are covered by the guardrail input.

In `@server/services/actions/scheduler.py`:
- Around line 26-37: The query in scheduler.py uses
db_pool.get_admin_connection() but does not set PostgreSQL RLS session vars, so
under FORCE RLS it returns no rows; after acquiring the connection from
get_admin_connection() and before running the SELECT in the with conn.cursor()
block, explicitly set the RLS session variables (e.g. set myapp.current_user_id
and myapp.current_org_id) on the connection using a SET / SELECT set_config(...)
call or equivalent connection API, using the intended system/admin user and org
identifiers for scheduled tasks, then run the SELECT; reference
db_pool.get_admin_connection(), the conn cursor context and the SELECT query
around "FROM actions a LEFT JOIN action_runs r" to locate where to add the RLS
setup.
- Around line 29-35: The scheduler SQL only checks MAX(r.started_at) to decide
"due" and can dispatch a new run while a prior run is still pending/running;
update the query in scheduler.py (the SELECT that returns a.id, a.org_id,
a.created_by, (a.trigger_config->>'interval_seconds')::int AS interval_seconds,
MAX(r.started_at) AS last_run_at) to exclude actions that currently have an
active run by adding a NOT EXISTS subquery (or LEFT JOIN + WHERE r2.status NOT
IN (...) logic) that filters out any action with an action_runs row where status
IN ('pending','running') (also apply the same change to the similar query around
lines 45-55) so the scheduler will skip dispatching when a previous run is still
active.

In `@server/utils/db/db_utils.py`:
- Around line 1191-1207: The action_runs table currently defines incident_id
UUID without a foreign key and uses an inefficient index idx_action_runs_status;
fix by adding a foreign key on action_runs.incident_id to reference
incidents(id) with ON DELETE SET NULL (matching chat_sessions.incident_id
behavior) and replace the existing idx_action_runs_status(org_id, status) with a
partial index on started_at for stale-run queries (e.g., CREATE INDEX ... ON
action_runs(started_at) WHERE status IN ('pending','running')) so the janitor
query in server/chat/background/task.py can use the index; update the table DDL
in action_runs and drop/replace the old index name idx_action_runs_status
accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fea8f318-c4bb-4d78-a7dc-1e312e964aca

📥 Commits

Reviewing files that changed from the base of the PR and between e4f7f73 and 2af8520.

📒 Files selected for processing (32)
  • client/src/app/actions/page.tsx
  • client/src/app/api/actions/[id]/route.ts
  • client/src/app/api/actions/[id]/run/route.ts
  • client/src/app/api/actions/[id]/runs/route.ts
  • client/src/app/api/actions/route.ts
  • client/src/app/chat/components/ChatClient.tsx
  • client/src/app/chat/components/useChatSendHandlers.ts
  • client/src/app/incidents/components/IncidentCard.tsx
  • client/src/components/SettingsModal.tsx
  • client/src/components/chat/SlashCommandMenu.tsx
  • client/src/components/chat/enhanced-chat-input.tsx
  • client/src/components/tool-calls/CommandLogo.tsx
  • client/src/lib/query.ts
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/prompt/background.py
  • server/chat/backend/agent/prompt/composer.py
  • server/chat/backend/agent/prompt/prompt_builder.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/tools/trigger_action_tool.py
  • server/chat/backend/agent/utils/state.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/main_chatbot.py
  • server/main_compute.py
  • server/routes/actions/__init__.py
  • server/routes/actions/actions_routes.py
  • server/services/actions/__init__.py
  • server/services/actions/executor.py
  • server/services/actions/scheduler.py
  • server/utils/auth/enforcer.py
  • server/utils/db/db_utils.py

Comment thread client/src/app/actions/page.tsx
Comment thread client/src/app/actions/page.tsx
Comment thread client/src/app/chat/components/useChatSendHandlers.ts
Comment thread client/src/app/chat/components/useChatSendHandlers.ts
Comment thread client/src/app/incidents/components/IncidentCard.tsx
Comment thread server/services/actions/executor.py
Comment thread server/services/actions/executor.py
Comment thread server/services/actions/scheduler.py
Comment thread server/services/actions/scheduler.py
Comment thread server/utils/db/db_utils.py
…ions

- dispatch_on_incident_actions now accepts a timing parameter
- Actions with trigger_config.timing='immediate' fire at incident creation
- Actions with trigger_config.timing='after_rca' fire when RCA completes
- UI shows timing selector when on_incident trigger type is chosen
- Default timing is after_rca to match original behavior
Comment thread server/services/actions/executor.py Outdated
Comment thread server/utils/db/db_utils.py
Comment thread server/routes/actions/actions_routes.py Outdated
Comment thread server/services/actions/executor.py Outdated
Comment thread server/chat/background/task.py
Comment thread server/chat/backend/agent/prompt/background.py
Comment thread server/chat/background/task.py Outdated
Comment thread client/src/app/chat/components/ChatClient.tsx Outdated
- Remove silent schedule clamping; show validation error for <5min intervals
- Use action name as fallback when triggering with empty text
- Escape key dismisses slash menu instead of wiping entire draft
- Clear selectedAction when user edits away the action name
- Fix polling: use queryClient.invalidate (bypasses staleTime)
- Guard conflicting forced-tool flags with elif
- Replace fragile string-matching with is_rca_background flag on PromptSegments
- Pin trigger_action tool to server-selected action_id via closure
- Only mark synthesized prompts as scaffolds for non-user sources
- Increase stale action janitor threshold from 20 to 35 minutes
- Add try/except to list_actions, update_action, list_runs routes
- Guard against None org_id in create_action
- Validate incident_id UUID in trigger_action endpoint
- Skip scheduler dispatch while previous run still active
- Wrap create_background_chat_session in try/except in executor
- Remove redundant _COL_FRAGMENTS guard
- Remove hardcoded tool names from action prompt
- Add comment explaining != action guard (loop prevention)
- Propagate guardrail_blocked flag to mark blocked actions as error
- Add revalidateOnEvents for actions cache invalidation
Comment thread client/src/app/actions/page.tsx Fixed
damianloch added 4 commits May 6, 2026 19:58
Removes the unused variable lint warning by referencing the derived
validation variable instead of repeating the inline condition.
The actions page fetcher unwraps the response to a plain array while
ChatClient expected {actions: [...]}. Since both share the same query
cache key, whichever fetched last determined the shape — causing the
slash menu to show empty when the actions page populated the cache first.
When guardrails block an action, the chat session was left empty (the
scaffold prompt is hidden). Now a bot message explaining the block is
appended so "View Chat" shows something useful.
- Replace window.dispatchEvent with globalThis (S7764, 2 instances)
- Use default params in closure to avoid loop variable capture (S1515)
- Extract duplicated "Internal server error" literal to _ERR_INTERNAL (S1192)

@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: 10

♻️ Duplicate comments (2)
client/src/app/incidents/components/IncidentCard.tsx (1)

126-140: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent non-OK responses; fetchR would normalize this.

handleOpen still uses raw fetch and only enters the populate branch on res.ok. HTTP errors (401/403/500) silently fall through, leaving the dropdown showing "No actions configured" with no toast — only network exceptions hit the catch. Switching to the fetchR already imported (and used by handleTrigger two functions below) routes both network and HTTP errors through the existing destructive toast and keeps loading-state cleanup in a finally.

🛠️ Proposed fix
-  const handleOpen = async () => {
-    if (open) { setOpen(false); return; }
-    setOpen(true);
-    setLoading(true);
-    try {
-      const res = await fetch('/api/actions?enabled=true', { credentials: 'include' });
-      if (res.ok) {
-        const data = await res.json();
-        setActions((data.actions || []).filter((a: { enabled: boolean }) => a.enabled));
-      }
-    } catch {
-      toast({ title: 'Failed to load actions', variant: 'destructive' });
-    }
-    setLoading(false);
-  };
+  const handleOpen = async () => {
+    if (open) { setOpen(false); return; }
+    setOpen(true);
+    setLoading(true);
+    try {
+      const data = await fetchR<{ actions?: { id: string; name: string; enabled: boolean }[] }>(
+        '/api/actions?enabled=true',
+      );
+      setActions((data.actions ?? []).filter((a) => a.enabled));
+    } catch {
+      toast({ title: 'Failed to load actions', variant: 'destructive' });
+    } finally {
+      setLoading(false);
+    }
+  };
🤖 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 `@client/src/app/incidents/components/IncidentCard.tsx` around lines 126 - 140,
The handleOpen function uses raw fetch which ignores non-2xx HTTP responses;
replace the fetch call with the shared fetchR utility (same one used by
handleTrigger) so HTTP errors throw and are handled by the existing error toast
flow, and move setLoading(false) into a finally block to ensure loading is
cleared on success, HTTP error, or network failure; update references in
handleOpen to parse the returned JSON via fetchR and still filter enabled
actions before calling setActions.
client/src/components/chat/enhanced-chat-input.tsx (1)

250-256: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the selected action when the command token stops matching, not just when the name disappears.

The current check only looks for selectedAction.name anywhere in the draft. If the user deletes /action <name> but still mentions the same words in normal prose, selectedAction stays set and the send path can still dispatch the old automation.

Suggested fix
-              if (selectedAction && !e.target.value.toLowerCase().includes(selectedAction.name.toLowerCase())) {
-                onActionSelect?.(null);
-              }
+              if (selectedAction) {
+                const escaped = selectedAction.name.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
+                const stillMatches = new RegExp(
+                  String.raw`(^|\s)\/actions?\s+${escaped}(\s|$)`,
+                  "i",
+                ).test(e.target.value);
+                if (!stillMatches) {
+                  onActionSelect?.(null);
+                }
+              }
🤖 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 `@client/src/components/chat/enhanced-chat-input.tsx` around lines 250 - 256,
The selectedAction is only cleared when its name appears nowhere in the draft;
instead, update the onChange handler in enhanced-chat-input.tsx to parse the
current command token from e.target.value (e.g., detect a leading "/" command +
argument token) and compare that token to selectedAction.name (or
selectedAction.trigger) — if the command token no longer matches the
selectedAction, call onActionSelect(null); keep the existing setInput,
setHighlightedIndex, and menuDismissed updates but replace the includes(...)
check with a strict command-token match against selectedAction.
🤖 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 `@client/src/app/api/actions/`[id]/run/route.ts:
- Line 6: The dynamic route param id is interpolated raw into the proxy path in
the call to forwardRequest (in route.ts) which can break routing for IDs with
special characters; change the interpolation to use encodeURIComponent(id) so
the path becomes something like `/api/actions/${encodeURIComponent(id)}/trigger`
when calling forwardRequest('POST', ... , 'action-trigger') to ensure IDs are
safely encoded.

In `@client/src/app/api/actions/`[id]/runs/route.ts:
- Line 6: The dynamic id is not URL-encoded before constructing the proxied
path, which can break route/query parsing; update the call in route.ts that uses
forwardRequest so the path uses encodeURIComponent(id) (e.g., change
`/api/actions/${id}/runs` to `/api/actions/${encodeURIComponent(id)}/runs`)
ensuring forwardRequest receives the encoded path and preserves correct routing.

In `@client/src/components/chat/enhanced-chat-input.tsx`:
- Around line 66-80: The code uses menuDismissed as a ref so mutations
(menuDismissed.current = true) do not trigger re-renders and useMemo never sees
changes; replace menuDismissed = useRef(false) with a boolean state like const
[menuDismissed, setMenuDismissed] = useState(false), update every place that
currently writes menuDismissed.current (e.g., the Escape handler) to call
setMenuDismissed(true/false), and include menuDismissed in the useMemo
dependency array so menuVisible/menuStage recompute when dismissal changes; also
update the other occurrences where menuDismissed.current is read or mutated to
use the state variable and setter.

In `@client/src/lib/query.ts`:
- Around line 308-315: The refresh logic in the useEffect uses
options?.refreshInterval with only a falsy check, which allows negative, NaN, or
Infinity to create tight loops or no-op timers; update the code around the
refreshInterval variable (used in the useEffect that calls
queryClient.invalidate with fetcherRef.current and optsRef.current) to validate
and normalize the value: compute a safeRefreshInterval by ensuring
Number.isFinite(options?.refreshInterval) and safeRefreshInterval > 0
(optionally Math.floor to an integer) and fall back to 0 if invalid, then use
that safeRefreshInterval in the useEffect and setInterval so invalid inputs
(negative, NaN, Infinity) do not schedule a timer.

In `@server/chat/background/task.py`:
- Around line 1275-1283: The code assumes row[0] is always a JSON string before
calling json.loads, which fails if messages is already parsed; change the read
path around cursor.execute/fetchone and the json.loads call to first check the
type of row[0] (e.g., if row and isinstance(row[0], (list, dict)) then use it
directly as messages, elif row and isinstance(row[0], str) then messages =
json.loads(row[0]) else messages = []), then append the new bot message and
perform the UPDATE via the existing cursor.execute("UPDATE chat_sessions SET
messages = %s::jsonb, updated_at = %s WHERE id = %s", (json.dumps(messages),
datetime.now(), session_id)).

In `@server/main_chatbot.py`:
- Around line 1223-1224: The assignment trigger_action_id =
data.get('trigger_action') accepts any JSON type; change it to validate and
normalize the payload (e.g., ensure it is a non-empty string and optionally
validate as a UUID) before storing or using it. Replace the direct assignment
with logic that checks isinstance(value, str) and value.strip() != "" (and if
desired validate with uuid.UUID) and only then set trigger_action_id; otherwise
set it to None (or skip activating the action-trigger flow) and log/handle the
invalid input. This validation should be applied wherever trigger_action_id is
later consumed in the handler code in main_chatbot.py to prevent non-string
values from triggering the action flow.

In `@server/routes/actions/actions_routes.py`:
- Around line 42-51: The _validate_trigger_config function currently accepts
non-object JSON (string/array/number) and then callers like create_action() may
call .get() on it; modify _validate_trigger_config to reject any trigger_config
that is not a dict (return None, "trigger_config must be an object") and only
process dicts for schedule logic (check for "interval_seconds" inside the dict
and validate as before), then update create_action() and the update path to call
and use _validate_trigger_config instead of accessing body["trigger_config"] or
calling .get() directly so malformed non-object values are rejected early and
never persisted.
- Around line 86-87: The helper _validate_enabled must require that
body["enabled"] is an actual JSON boolean rather than coercing strings; change
_validate_enabled to check isinstance(body.get("enabled"), bool) and return
(body["enabled"], None) when it's a bool, otherwise return (None, <validation
error object/string>) so malformed values like "false" or "0" produce a 400
instead of silently enabling the action; ensure you reference _validate_enabled
and body["enabled"] when making the change.
- Around line 365-368: The route currently parses limit and offset but allows
negative limit (e.g., limit = -1) which later causes a DB 500; after parsing the
values (the parsed variables limit and offset from request.args.get) add a
validation that rejects negative limits by returning a 400 JSON error (use
jsonify) if limit < 0 (and similarly ensure offset is not negative if desired),
so the handler validates inputs before building the SQL query.

In `@server/services/actions/executor.py`:
- Around line 120-123: The loop that reads trigger_config currently defaults
missing trigger_config.timing to "immediate" (in the for action_id,
trigger_config in rows block setting action_timing = (trigger_config or
{}).get("timing", "immediate")), causing legacy/manual on_incident rows to run
on RCA start; change the behavior to treat missing timing as "completion" (or
require explicit "immediate" in the write path) by defaulting action_timing to
"completion" when trigger_config.timing is absent so only rows explicitly marked
immediate run on start and others fire after completion.

---

Duplicate comments:
In `@client/src/app/incidents/components/IncidentCard.tsx`:
- Around line 126-140: The handleOpen function uses raw fetch which ignores
non-2xx HTTP responses; replace the fetch call with the shared fetchR utility
(same one used by handleTrigger) so HTTP errors throw and are handled by the
existing error toast flow, and move setLoading(false) into a finally block to
ensure loading is cleared on success, HTTP error, or network failure; update
references in handleOpen to parse the returned JSON via fetchR and still filter
enabled actions before calling setActions.

In `@client/src/components/chat/enhanced-chat-input.tsx`:
- Around line 250-256: The selectedAction is only cleared when its name appears
nowhere in the draft; instead, update the onChange handler in
enhanced-chat-input.tsx to parse the current command token from e.target.value
(e.g., detect a leading "/" command + argument token) and compare that token to
selectedAction.name (or selectedAction.trigger) — if the command token no longer
matches the selectedAction, call onActionSelect(null); keep the existing
setInput, setHighlightedIndex, and menuDismissed updates but replace the
includes(...) check with a strict command-token match against selectedAction.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2f4094c-da73-4d36-af00-968e4cad8a98

📥 Commits

Reviewing files that changed from the base of the PR and between 2af8520 and 3c2e333.

📒 Files selected for processing (33)
  • client/src/app/actions/page.tsx
  • client/src/app/api/actions/[id]/route.ts
  • client/src/app/api/actions/[id]/run/route.ts
  • client/src/app/api/actions/[id]/runs/route.ts
  • client/src/app/api/actions/route.ts
  • client/src/app/chat/components/ChatClient.tsx
  • client/src/app/chat/components/useChatSendHandlers.ts
  • client/src/app/incidents/components/IncidentCard.tsx
  • client/src/components/SettingsModal.tsx
  • client/src/components/chat/SlashCommandMenu.tsx
  • client/src/components/chat/enhanced-chat-input.tsx
  • client/src/components/tool-calls/CommandLogo.tsx
  • client/src/lib/query.ts
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/prompt/background.py
  • server/chat/backend/agent/prompt/composer.py
  • server/chat/backend/agent/prompt/prompt_builder.py
  • server/chat/backend/agent/prompt/schema.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/tools/trigger_action_tool.py
  • server/chat/backend/agent/utils/state.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/main_chatbot.py
  • server/main_compute.py
  • server/routes/actions/__init__.py
  • server/routes/actions/actions_routes.py
  • server/services/actions/__init__.py
  • server/services/actions/executor.py
  • server/services/actions/scheduler.py
  • server/utils/auth/enforcer.py
  • server/utils/db/db_utils.py

Comment thread client/src/app/api/actions/[id]/run/route.ts
Comment thread client/src/app/api/actions/[id]/runs/route.ts
Comment thread client/src/components/chat/enhanced-chat-input.tsx
Comment thread client/src/lib/query.ts
Comment thread server/chat/background/task.py
Comment thread server/main_chatbot.py Outdated
Comment thread server/routes/actions/actions_routes.py
Comment thread server/routes/actions/actions_routes.py Outdated
Comment thread server/routes/actions/actions_routes.py Outdated
Comment thread server/services/actions/executor.py
OlivierTrudeau
OlivierTrudeau previously approved these changes May 7, 2026
- Handle messages column as list or string in _append_block_message
- Validate trigger_action is a non-empty string before activating flow
- Reject non-dict trigger_config values with 400
- Require strict boolean for enabled field
- Clamp negative limit to 1 in list_runs
@sonarqubecloud

sonarqubecloud Bot commented May 7, 2026

Copy link
Copy Markdown

@damianloch
damianloch merged commit 85f5658 into main May 7, 2026
16 checks passed
@damianloch
damianloch deleted the feat/actions-system branch May 7, 2026 19:31
OlivierTrudeau added a commit that referenced this pull request May 9, 2026
* feat: add Actions system for user-defined background agent tasks

Adds a general-purpose Actions feature where users define natural language
instructions that Aurora executes as background agent tasks using connected
tools (IaC, GitHub, Datadog, etc.). Replaces the need for hardcoded
automation pipelines.

Backend:
- actions + action_runs DB tables with RLS
- CRUD + trigger routes at /api/actions
- Executor service (dispatch_action, build_action_prompt)
- Completion hooks in run_background_chat (all exit paths + finally)
- Dedicated RBAC resource for actions (viewer read, editor write)

Frontend:
- Full actions page with list/detail/create views
- API proxy routes
- Run Action dropdown on IncidentCard (post-RCA trigger)
- Sidebar nav entry

* feat: add action editing and scheduled trigger type

- Refactor CreateActionView into ActionFormView supporting create + edit
- Add Edit button in action detail view header
- Add on_schedule trigger type with interval picker (min 5 minutes)
- Add scheduler Celery beat task (runs every 60s, dispatches due actions)

* fix: clean up stale action runs in background chat cleanup task

Adds a 4th step to cleanup_stale_background_chats that marks any
action_runs stuck in running/pending for >20 minutes as error.
Handles container restarts where the finally block never executed.

* fix: set RLS context in action executor and cleanup for Celery paths

dispatch_action, _create_run, and _update_run now call set_rls_context
so they work when invoked from the scheduler (no Flask request context).
Stale action runs cleanup iterates per-org to respect RLS policies.

* fix: resolve PUT 500 error and deprecation warnings in actions

- Extract _get_action_response() helper to avoid calling decorated
  get_action() from update_action() (TypeError: too many args)
- Replace datetime.utcnow() with datetime.now(timezone.utc) in both
  routes and executor

* prompt changes

* move actions to settings

* feat: add /action slash command to chat with autocomplete

Users can type /action <name> in chat to trigger an Aurora Action
without leaving the conversation. Includes autocomplete dropdown,
comprehensive error handling (404, 403, 429, network errors),
and graceful empty-state messaging.

* fix: slash command UX - arrow nav, action chip, post-select typing

- Arrow Up/Down navigates the autocomplete dropdown
- Enter/Tab selects highlighted action
- Selected action renders as a purple chip (like incident context)
- Input clears after selection so user can type additional context
- Submit uses stored action ID directly (no fragile text re-parsing)
- Text-based /action fallback still works for power users

* progress

* fix: action trigger UX polish and timestamp handling

- Remove action instruction prepend from user messages to prevent
  system prompt leaking into chat UI (action_id passed via tool desc)
- Fix timestamps showing "just now" by appending Z suffix to isoformat
- Use formatTimeAgo for consistent relative time display
- Fix actions.filter crash when shared query cache returns object shape
- Add trigger_action icon matching Settings modal Workflow icon

* fix: resolve SonarQube security hotspots and reliability bug

- Replace backtrack-prone regex patterns (\s+ followed by .+/.*?) with
  single \s followed by .* to eliminate super-linear runtime risk
- Remove dead conditional that returned same value for both branches

* fix: eliminate all regex backtracking patterns for SonarQube S5852

Replace regex-based parsing with string operations (startsWith, search
+ slice) to fully avoid patterns Sonar flags as backtrack-vulnerable.

* fix: resolve SonarQube vulnerabilities and code smells

- Remove user-controlled data from log messages (S5145 log injection)
- Replace empty except blocks with debug logging
- Remove unused _VALID_MODES constant and unused user_id param
- Remove unused fetchR import
- Replace regex with string operations to avoid backtracking (S5852)

* fix: address remaining CodeQL and SonarCloud findings

- Don't expose exception details to client (CodeQL info exposure)
- Remove unused constants (_VALID_TRIGGER_TYPES, _VALID_MODES)
- Remove unused run_id assignment in rate limit path
- Replace all empty except:pass with debug logging

* fix: remove redundant conditional check in chat input

* refactor: reduce sendMessage cognitive complexity

Extract parseActionFromText and ensureSession helpers to flatten
the sendMessage function and bring cognitive complexity under 15.

* fix: address SonarCloud code smells across frontend and backend

Frontend:
- Mark all component props as readonly
- Extract nested ternaries into helper functions/components
- Replace .match() with RegExp.exec()
- Extract ActionOverlay component (fixes keys, String.raw, complexity)
- Fix negated condition (use strict equality)
- Extract renderView and getTriggerDescription helpers

Backend:
- Define _ERR_NOT_FOUND constant to deduplicate literal

* refactor: reduce cognitive complexity across actions code

Extract helpers to bring functions under SonarCloud's complexity
threshold: _parse_update_fields, _validate_create_fields (routes),
_is_due (scheduler), _add_missing_policies (enforcer), handleMenuKey
+ useMemo (enhanced-chat-input), nested ternary fix (useChatSendHandlers).
Also use String.raw and replaceAll per Sonar suggestions.

* fix: RLS context for incident loading, safe query params, generic error messages

- _load_incident_context now receives user_id and sets RLS context
  (incidents table is RLS-protected, previously returned 0 rows from Celery)
- list_runs wraps int() parsing in try/except to avoid unhandled 500
- trigger_action_tool returns generic errors instead of raw exception strings
  and uses parameterized logging

* fix: validate action_id as UUID, whitelist SQL columns

- All routes accepting action_id now validate UUID format before
  processing, preventing injection and XSS (CodeQL findings)
- UPDATE query uses _UPDATABLE_COLUMNS frozenset to construct SET clause
  exclusively from allowed column names (addresses SQL injection finding)
- Clean up import order

* fix: break CodeQL taint chains in actions routes

- Helper functions now return error strings (not response tuples),
  breaking the taint flow from request body to response
- SQL SET clause built from frozenset-filtered column names only
- delete_action wrapped in try/except to prevent stack trace exposure

* fix: use constant dict lookup for SQL SET clause (CodeQL)

Column names from _parse_update_fields are used only as dictionary keys
to retrieve constant SQL fragments. No user-derived value enters the
query string, fully breaking the taint chain.

* fix: remove unused _UPDATABLE_COLUMNS, use _COL_FRAGMENTS at module level

* refactor: reduce cognitive complexity in composer and actions routes

- assemble_system_prompt: replace sequential if-checks with loop over
  ordered optional segments (16 -> ~10)
- _parse_update_fields: extract _validate_trigger_config helper to
  reduce nesting depth (31 -> ~14)

* refactor: extract field validators to reduce _parse_update_fields complexity

* refactor: loop-based _parse_update_fields to eliminate branching complexity

* fix: compute duration before string conversion, eliminate regex backtracking

- _get_action_response: compute duration_ms from datetime objects before
  converting to isoformat strings (avoids wasteful parse-back)
- handleSelect: replace backtrack-prone regex with indexOf + slice

* fix: extract nested ternary and remove negated condition in handleSelect

* fix: add error handling to handleToggle, confirmation to handleDelete

* fix: memoize clearSelectedAction to avoid unnecessary re-renders

* fix: remove redundant trim() call on already-trimmed string

* fix: add click-outside handler and error toast to RunActionDropdown

* fix: guard against undefined action in menu Enter/Tab handler

* fix: use parameterized logging in build_action_mode_segment

* fix: prevent negative duration_ms by explicitly setting started_at on immediate-failure runs

* feat: dispatch on_incident actions automatically when incidents are created

- Add dispatch_on_incident_actions() that finds enabled on_incident
  actions and dispatches each with full incident context
- Hook into run_background_chat at RCA start (after incident linking)
  with source != 'action' guard to prevent infinite loops
- Add duration_ms to list_runs endpoint (was missing)

* fix: update on_incident trigger label to reflect immediate dispatch

* feat: support both immediate and after_rca timing for on_incident actions

- dispatch_on_incident_actions now accepts a timing parameter
- Actions with trigger_config.timing='immediate' fire at incident creation
- Actions with trigger_config.timing='after_rca' fire when RCA completes
- UI shows timing selector when on_incident trigger type is chosen
- Default timing is after_rca to match original behavior

* fix: address PR #366 review feedback (20 fixes)

- Remove silent schedule clamping; show validation error for <5min intervals
- Use action name as fallback when triggering with empty text
- Escape key dismisses slash menu instead of wiping entire draft
- Clear selectedAction when user edits away the action name
- Fix polling: use queryClient.invalidate (bypasses staleTime)
- Guard conflicting forced-tool flags with elif
- Replace fragile string-matching with is_rca_background flag on PromptSegments
- Pin trigger_action tool to server-selected action_id via closure
- Only mark synthesized prompts as scaffolds for non-user sources
- Increase stale action janitor threshold from 20 to 35 minutes
- Add try/except to list_actions, update_action, list_runs routes
- Guard against None org_id in create_action
- Validate incident_id UUID in trigger_action endpoint
- Skip scheduler dispatch while previous run still active
- Wrap create_background_chat_session in try/except in executor
- Remove redundant _COL_FRAGMENTS guard
- Remove hardcoded tool names from action prompt
- Add comment explaining != action guard (loop prevention)
- Propagate guardrail_blocked flag to mark blocked actions as error
- Add revalidateOnEvents for actions cache invalidation

* fix: use intervalTooLow variable in submit button disabled check

Removes the unused variable lint warning by referencing the derived
validation variable instead of repeating the inline condition.

* fix: handle both cache shapes for /api/actions in chat slash menu

The actions page fetcher unwraps the response to a plain array while
ChatClient expected {actions: [...]}. Since both share the same query
cache key, whichever fetched last determined the shape — causing the
slash menu to show empty when the actions page populated the cache first.

* fix: persist visible message in chat when action is guardrail-blocked

When guardrails block an action, the chat session was left empty (the
scaffold prompt is hidden). Now a bot message explaining the block is
appended so "View Chat" shows something useful.

* fix: resolve 5 SonarCloud code smells

- Replace window.dispatchEvent with globalThis (S7764, 2 instances)
- Use default params in closure to avoid loop variable capture (S1515)
- Extract duplicated "Internal server error" literal to _ERR_INTERNAL (S1192)

* Implement action like postmortem and version control

* Make slack compatible

* Add slack logo and postmortem and aslo tool parsing

* remove the postmortem parser

* Add system actions and the postmortem generation as a system action

* address sonarqube

* fix

* partial to comments

* fix

* fix the versioning

* fixees

* fixes

* address

---------

Co-authored-by: Damian Loch <damian.loch@arvoai.ca>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants