Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e39aa23
feat: add change_gating_enabled column and enrollment API to repo sel…
isiddharthsingh Jun 12, 2026
e4892ae
feat: per-run tool denylist plumbed through agent state, background c…
isiddharthsingh Jun 12, 2026
6d6e224
feat: change-gating service layer (GitHub PR adapter, diff utils, ver…
isiddharthsingh Jun 12, 2026
9b84cd5
feat: PR change-gating webhook filters and investigate_pr Celery task
isiddharthsingh Jun 12, 2026
8fff13d
test: change-gating unit tests (adapter, diff utils, verdict, webhook…
isiddharthsingh Jun 12, 2026
5df452b
feat: Incident Prevention toggle on connected repos
isiddharthsingh Jun 12, 2026
7b3d7a4
chore: add CHANGE_GATING_DRY_RUN env var across compose files and .en…
isiddharthsingh Jun 12, 2026
94a0cdd
feat: transient 'Aurora is reviewing' progress comment on PRs
isiddharthsingh Jun 12, 2026
a465743
feat: incremental CodeRabbit-style inline review comments (post net-n…
isiddharthsingh Jun 12, 2026
c1a02fe
feat: incremental-diff PR reviews (review only new commits, status-ga…
isiddharthsingh Jun 12, 2026
9d2fc90
fix: address CodeRabbit review (dedupe-key release on skip, dup-log g…
isiddharthsingh Jun 12, 2026
c53f8ad
ci: install PyJWT in pre-checks so change_gating adapter/verdict test…
isiddharthsingh Jun 12, 2026
9919b9c
fix: address PR #506 review (quality-gate: secret-scan/ReDoS/reliabil…
isiddharthsingh Jun 12, 2026
c84212b
refactor: clear remaining sonar nits on PR #506 (logging.exception, d…
isiddharthsingh Jun 12, 2026
0a8125f
feat: scope change-gating review to infra/deploy/CI-CD risk with per-…
isiddharthsingh Jun 15, 2026
656b8b6
feat: split github oauth login vs token-honoring and auto-import inst…
isiddharthsingh Jun 15, 2026
4749f84
fix: use real github logo and drop oauth installation skeleton
isiddharthsingh Jun 15, 2026
61234c4
fix: make import_installation_repos body celery-agnostic so CI (stubb…
isiddharthsingh Jun 15, 2026
4bff8ea
refactor: extract per-file diff block helper (cognitive complexity) a…
isiddharthsingh Jun 15, 2026
ae37165
docs: add GitHub App setup guide to Docusaurus and inline permissions…
isiddharthsingh Jun 16, 2026
35ab3b0
fix: close adapter session in finally, guard pagination type, and esc…
isiddharthsingh Jun 17, 2026
fa1203b
refactor: remove tool denylist, add verdict-source logging, reduce ti…
isiddharthsingh Jun 17, 2026
c15fc73
feat: rewrite review prompt to leverage live infrastructure context a…
isiddharthsingh Jun 17, 2026
168501a
refactor: replace tool_denylist with is_pr_review context flag and re…
isiddharthsingh Jun 17, 2026
e2c0bd5
Update github-provider-integration.tsx
damianloch Jun 17, 2026
731d8ae
feat: add NEXT_PUBLIC_ENABLE_INCIDENT_PREVENTION feature flag replaci…
isiddharthsingh Jun 17, 2026
9691823
fix: remove unused os import from change_gating.py
isiddharthsingh Jun 17, 2026
21dd4ac
feat: add verdict decision test to reduce review sensitivity and fals…
isiddharthsingh Jun 17, 2026
95b5eb0
fix: skip NeMo input guardrail for PR change-gating reviews (content-…
isiddharthsingh Jun 18, 2026
ea76cbf
docs: rework GitHub App setup per review; add missing GitHub vars to …
isiddharthsingh Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ GITHUB_APP_WEBHOOK_URL=
GITHUB_APP_SETUP_URL=
# Fallback used when the value is not stored in Vault.
GITHUB_APP_WEBHOOK_SECRET=
# Log PR change-gating reviews instead of posting to GitHub (calibration mode)
CHANGE_GATING_DRY_RUN=false
Comment thread
damianloch marked this conversation as resolved.
Outdated

# GitHub OAuth (only required when GITHUB_AUTH_MODE=oauth or =hybrid).
# Create at https://github.com/settings/developers > New OAuth App.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
python-version: "3.12"

- name: Install test dependencies
run: pip install pytest==9.0.3 Flask==3.1.3 psycopg2-binary==2.9.12 python-dotenv==1.2.2 pyyaml==6.0.1 pydantic==2.13.3 langchain-core==1.2.31
run: pip install pytest==9.0.3 Flask==3.1.3 psycopg2-binary==2.9.12 python-dotenv==1.2.2 pyyaml==6.0.1 pydantic==2.13.3 langchain-core==1.2.31 PyJWT==2.13.0

- name: Run all tests
working-directory: server
Expand Down
65 changes: 65 additions & 0 deletions client/src/components/github-provider-integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import {
AlertDialog,
Expand Down Expand Up @@ -76,6 +77,9 @@
metadata_status: string;
repo_data: Repository | null;
created_at: string | null;
// PR change gating (incident prevention) — only settable on App-linked repos.
change_gating_enabled?: boolean;
installation_id?: number | null;
}

export interface GitHubAuthConfig {
Expand Down Expand Up @@ -188,6 +192,18 @@
if (!response.ok) throw new Error('Failed to update metadata');
}

static async setChangeGating(repoFullName: string, enabled: boolean): Promise<void> {
const response = await fetch(`/api/proxy/github/repo-selections/${encodeURIComponent(repoFullName)}/change-gating`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
});
if (!response.ok) {
const data = await response.json().catch(() => null);
throw new Error(data?.error || 'Failed to update incident prevention setting');
}
}

static async generateRepoMetadata(repoFullName: string): Promise<void> {
const response = await fetch('/api/proxy/github/repo-metadata/generate', {
method: 'POST',
Expand Down Expand Up @@ -230,6 +246,7 @@
const [savedRepos, setSavedRepos] = useState<ConnectedRepo[]>([]);
const [savedReposLoaded, setSavedReposLoaded] = useState(false);
const [editingMetadata, setEditingMetadata] = useState<Record<string, string>>({});
const [gatingUpdating, setGatingUpdating] = useState<Set<string>>(new Set());
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
const popupCleanupsRef = useRef<Array<() => void>>([]);

Expand Down Expand Up @@ -526,6 +543,39 @@
}
};

const handleChangeGatingToggle = async (repoFullName: string, enabled: boolean) => {
setGatingUpdating(prev => new Set(prev).add(repoFullName));
setSavedRepos(prev => prev.map(r =>
r.repo_full_name === repoFullName ? { ...r, change_gating_enabled: enabled } : r
));
try {
await GitHubIntegrationService.setChangeGating(repoFullName, enabled);
// Re-assert the confirmed value: a loadSavedRepos poll snapshotted
// before the PUT committed can land after the optimistic update and
// clobber it with the stale flag.
setSavedRepos(prev => prev.map(r =>
r.repo_full_name === repoFullName ? { ...r, change_gating_enabled: enabled } : r
));
window.dispatchEvent(new CustomEvent('providerStateChanged'));

Check warning on line 559 in client/src/components/github-provider-integration.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=Arvo-AI_aurora&issues=AZ69OctW6-8DAyAu-wDM&open=AZ69OctW6-8DAyAu-wDM&pullRequest=506
} catch (error: unknown) {
const err = error as Error;
setSavedRepos(prev => prev.map(r =>
r.repo_full_name === repoFullName ? { ...r, change_gating_enabled: !enabled } : r
));
toast({
title: "Error",
description: err.message || "Failed to update incident prevention setting",
variant: "destructive",
});
} finally {
setGatingUpdating(prev => {
const next = new Set(prev);
next.delete(repoFullName);
return next;
});
}
};

const handleRegenerate = async (repoFullName: string) => {
try {
await GitHubIntegrationService.generateRepoMetadata(repoFullName);
Expand Down Expand Up @@ -1135,6 +1185,21 @@
{isReady && !isEditing && repo.metadata_summary && (
<p className="text-xs text-muted-foreground">{repo.metadata_summary.replace(/\*\*/g, '')}</p>
)}
{repo.installation_id != null && (
<div
className="flex items-center justify-between gap-2 pt-1"
title="Incident Prevention — Aurora reviews PRs for incident risk"
>
<span className="text-xs text-muted-foreground">Incident Prevention</span>
<Switch
checked={!!repo.change_gating_enabled}
disabled={gatingUpdating.has(repo.repo_full_name)}
onCheckedChange={(checked) => handleChangeGatingToggle(repo.repo_full_name, checked)}
className="scale-75 origin-right"
data-testid={`repo-change-gating-${repo.repo_full_name}`}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
)}
</div>
);
})}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.airtight.yml
Comment thread
isiddharthsingh marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ x-common-env: &common-env
NEXT_PUBLIC_ENABLE_SHAREPOINT: ${NEXT_PUBLIC_ENABLE_SHAREPOINT}
NEXT_PUBLIC_ENABLE_NOTION: ${NEXT_PUBLIC_ENABLE_NOTION}
NEXT_PUBLIC_ENABLE_SPINNAKER: ${NEXT_PUBLIC_ENABLE_SPINNAKER}
# Log PR change-gating reviews instead of posting to GitHub (calibration mode)
CHANGE_GATING_DRY_RUN: ${CHANGE_GATING_DRY_RUN:-false}
# Slack OAuth (needed by celery_worker for formatting responses)
SLACK_CLIENT_ID: ${SLACK_CLIENT_ID}
SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.prod-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ x-common-env: &common-env
GITHUB_APP_SETUP_URL: ${GITHUB_APP_SETUP_URL}
GH_OAUTH_CLIENT_ID: ${GH_OAUTH_CLIENT_ID}
GH_OAUTH_CLIENT_SECRET: ${GH_OAUTH_CLIENT_SECRET}
# Log PR change-gating reviews instead of posting to GitHub (calibration mode)
CHANGE_GATING_DRY_RUN: ${CHANGE_GATING_DRY_RUN:-false}

# AI Safety Guardrails
GUARDRAILS_ENABLED: ${GUARDRAILS_ENABLED:-true}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ x-common-env: &common-env
GITHUB_APP_SETUP_URL: ${GITHUB_APP_SETUP_URL}
GH_OAUTH_CLIENT_ID: ${GH_OAUTH_CLIENT_ID}
GH_OAUTH_CLIENT_SECRET: ${GH_OAUTH_CLIENT_SECRET}
# Log PR change-gating reviews instead of posting to GitHub (calibration mode)
CHANGE_GATING_DRY_RUN: ${CHANGE_GATING_DRY_RUN:-false}

# AI Safety Guardrails
GUARDRAILS_ENABLED: ${GUARDRAILS_ENABLED:-true}
Expand Down
1 change: 1 addition & 0 deletions server/celery_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
'utils.aws.credential_refresh',
'routes.aws.cloudwatch_tasks',
'tasks.github_webhook_tasks',
'tasks.change_gating',
'routes.github.github_repo_metadata',
'utils.repo_metadata',
'services.actions.scheduler',
Expand Down
8 changes: 5 additions & 3 deletions server/chat/backend/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from chat.backend.agent.model_mapper import ModelMapper
from chat.backend.agent.providers import create_chat_model, get_registry
from chat.backend.agent.weaviate_client import WeaviateClient
from chat.backend.agent.utils.state import State
from chat.backend.agent.utils.state import State, filter_denied_tools
from chat.backend.agent.utils.tool_context_capture import ToolContextCapture
from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI
Expand Down Expand Up @@ -355,8 +355,10 @@ async def agentic_tool_flow(
tools = get_cloud_tools()
if tool_subset is not None:
tools = tool_subset


# Drop denylisted tools (returns a new list — get_cloud_tools() result is cached)
tools = filter_denied_tools(tools, state.tool_denylist)


prompt_text = ''
if state.messages and hasattr(state.messages[-1], 'content'):
# Handle both string and multimodal content
Expand Down
1 change: 1 addition & 0 deletions server/chat/backend/agent/orchestrator/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ def _build_sends(state: State) -> list:
"parent_user_id": user_id,
"parent_org_id": org_id,
"parent_session_id": parent_session_id,
"parent_tool_denylist": getattr(state, "tool_denylist", None),
"wave": wave,
}
sends.append(Send("sub_agent", payload))
Expand Down
4 changes: 4 additions & 0 deletions server/chat/backend/agent/orchestrator/sub_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ async def _run(input_dict: dict) -> FindingRef:
is_background=True,
mode="ask",
model=sub_agent_model,
# A parent run's denylist must survive the sub-agent boundary,
# or denied write/exec tools would silently reappear here
# (agentic_tool_flow filters by sub_state.tool_denylist).
tool_denylist=input_dict.get("parent_tool_denylist"),
)

postgres_client = PostgreSQLClient()
Expand Down
16 changes: 16 additions & 0 deletions server/chat/backend/agent/utils/state.py
Comment thread
damianloch marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
from pydantic import BaseModel, ConfigDict


def filter_denied_tools(tools: List[Any], tool_denylist: Optional[List[str]]) -> List[Any]:
"""Return ``tools`` minus those whose ``.name`` is in ``tool_denylist``.

For a non-empty denylist, returns a NEW filtered list (the input may be
the cached ``get_cloud_tools()`` list, which must never be mutated). For
an empty/None denylist, returns the input list object unchanged (callers
must not mutate it). Single source of truth for ``State.tool_denylist``
semantics — used by ``agentic_tool_flow`` and unit-tested directly.
"""
if not tool_denylist:
return tools
denied = set(tool_denylist)
return [t for t in tools if getattr(t, "name", None) not in denied]


class State(BaseModel):
messages: List[AnyMessage] = []
question: str
Expand Down Expand Up @@ -41,6 +56,7 @@ class State(BaseModel):
)
guardrail_blocked: bool = False # Set by workflow when input rail blocks the message
permitted_tools: Optional[set] = None
tool_denylist: Optional[List[str]] = None # Tool names removed from the tool set for this run

# --- Multi-agent orchestrator fields (defaults preserve single-agent behavior) ---
triage_decision: Optional[Dict[str, Any]] = None
Expand Down
9 changes: 9 additions & 0 deletions server/chat/background/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ def run_background_chat(
send_notifications: bool = True,
mode: str = "ask",
rail_text: Optional[str] = None,
tool_denylist: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Run a chat session in the background without WebSocket.

Expand All @@ -446,6 +447,8 @@ def run_background_chat(
only the externally-controlled fields should be checked for prompt
injection; the internal instruction scaffolding should not. When
omitted, falls back to initial_message (legacy behavior).
tool_denylist: Optional list of tool names to remove from the agent's
tool set for this run (e.g. write/exec tools for PR change gating).

Returns:
Dict with session_id, status, and any error information
Expand Down Expand Up @@ -663,6 +666,7 @@ def run_background_chat(
incident_id=incident_id,
mode=mode,
rail_text=rail_text,
tool_denylist=tool_denylist,
))
except Exception as e:
logger.error(f"[BackgroundChat] Exception in asyncio.run(_execute_background_chat): {e}", exc_info=True)
Expand Down Expand Up @@ -1144,6 +1148,7 @@ async def _run_jira_action(
mode: str,
wf,
background_ws,
tool_denylist: Optional[List[str]] = None,
) -> None:
"""Run the Jira filing step after the RCA investigation completes.

Expand Down Expand Up @@ -1194,6 +1199,7 @@ async def _run_jira_action(
mode=mode,
is_background=True,
rca_context=rca_context,
tool_denylist=tool_denylist,
)
logger.info(f"[JiraAction] Starting Jira step for {session_id} (jira_mode={jira_mode})")

Expand Down Expand Up @@ -1239,6 +1245,7 @@ async def _execute_background_chat(
incident_id: Optional[str] = None,
mode: str = "ask",
rail_text: Optional[str] = None,
tool_denylist: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Execute the background chat workflow asynchronously.

Expand Down Expand Up @@ -1370,6 +1377,7 @@ async def _execute_background_chat(
is_postmortem_action=_is_postmortem_action,
rca_context=rca_context,
permitted_tools=_resolve_permitted_tools(user_id),
tool_denylist=tool_denylist,
)
logger.info(
f"[BackgroundChat] Created state with is_background=True, is_postmortem_action={_is_postmortem_action}, "
Expand Down Expand Up @@ -1420,6 +1428,7 @@ async def _execute_background_chat(
mode=mode,
wf=wf,
background_ws=background_ws,
tool_denylist=tool_denylist,
)

if incident_id:
Expand Down
Loading
Loading