Add Aurora Actions — user-defined background automations - #366
Conversation
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughIntroduces 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. ChangesActions Management System
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 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)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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 |
|
❌ The last analysis has failed. |
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)
… immediate-failure runs
…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)
d794472 to
6ca2580
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (32)
client/src/app/actions/page.tsxclient/src/app/api/actions/[id]/route.tsclient/src/app/api/actions/[id]/run/route.tsclient/src/app/api/actions/[id]/runs/route.tsclient/src/app/api/actions/route.tsclient/src/app/chat/components/ChatClient.tsxclient/src/app/chat/components/useChatSendHandlers.tsclient/src/app/incidents/components/IncidentCard.tsxclient/src/components/SettingsModal.tsxclient/src/components/chat/SlashCommandMenu.tsxclient/src/components/chat/enhanced-chat-input.tsxclient/src/components/tool-calls/CommandLogo.tsxclient/src/lib/query.tsserver/celery_config.pyserver/chat/backend/agent/agent.pyserver/chat/backend/agent/prompt/background.pyserver/chat/backend/agent/prompt/composer.pyserver/chat/backend/agent/prompt/prompt_builder.pyserver/chat/backend/agent/tools/cloud_tools.pyserver/chat/backend/agent/tools/trigger_action_tool.pyserver/chat/backend/agent/utils/state.pyserver/chat/backend/agent/workflow.pyserver/chat/background/task.pyserver/main_chatbot.pyserver/main_compute.pyserver/routes/actions/__init__.pyserver/routes/actions/actions_routes.pyserver/services/actions/__init__.pyserver/services/actions/executor.pyserver/services/actions/scheduler.pyserver/utils/auth/enforcer.pyserver/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
- 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
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)
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (2)
client/src/app/incidents/components/IncidentCard.tsx (1)
126-140:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSilent non-OK responses;
fetchRwould normalize this.
handleOpenstill uses rawfetchand only enters the populate branch onres.ok. HTTP errors (401/403/500) silently fall through, leaving the dropdown showing "No actions configured" with no toast — only network exceptions hit thecatch. Switching to thefetchRalready imported (and used byhandleTriggertwo functions below) routes both network and HTTP errors through the existing destructive toast and keeps loading-state cleanup in afinally.🛠️ 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 winClear the selected action when the command token stops matching, not just when the name disappears.
The current check only looks for
selectedAction.nameanywhere in the draft. If the user deletes/action <name>but still mentions the same words in normal prose,selectedActionstays 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
📒 Files selected for processing (33)
client/src/app/actions/page.tsxclient/src/app/api/actions/[id]/route.tsclient/src/app/api/actions/[id]/run/route.tsclient/src/app/api/actions/[id]/runs/route.tsclient/src/app/api/actions/route.tsclient/src/app/chat/components/ChatClient.tsxclient/src/app/chat/components/useChatSendHandlers.tsclient/src/app/incidents/components/IncidentCard.tsxclient/src/components/SettingsModal.tsxclient/src/components/chat/SlashCommandMenu.tsxclient/src/components/chat/enhanced-chat-input.tsxclient/src/components/tool-calls/CommandLogo.tsxclient/src/lib/query.tsserver/celery_config.pyserver/chat/backend/agent/agent.pyserver/chat/backend/agent/prompt/background.pyserver/chat/backend/agent/prompt/composer.pyserver/chat/backend/agent/prompt/prompt_builder.pyserver/chat/backend/agent/prompt/schema.pyserver/chat/backend/agent/tools/cloud_tools.pyserver/chat/backend/agent/tools/trigger_action_tool.pyserver/chat/backend/agent/utils/state.pyserver/chat/backend/agent/workflow.pyserver/chat/background/task.pyserver/main_chatbot.pyserver/main_compute.pyserver/routes/actions/__init__.pyserver/routes/actions/actions_routes.pyserver/services/actions/__init__.pyserver/services/actions/executor.pyserver/services/actions/scheduler.pyserver/utils/auth/enforcer.pyserver/utils/db/db_utils.py
- 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
|
* 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>



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
Frontend
/actionslash command in chat with autocomplete, inline styling, and tool-call integrationChat integration
/action <name>triggers via LLM tool call (same pattern as RCA trigger)Test plan
/actioncommand in chat, verify tool call renders with correct iconSummary by CodeRabbit