Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c6b0423
feat(frontend): add Playwright E2E tests with CI workflow (#2279)
yangzheli Apr 18, 2026
ca1b7d5
fix(sandbox): add missing path masking in ls_tool output (#2317)
wisetwo Apr 18, 2026
3b91df2
fix(frontend): add catch-all API rewrite for gateway routes (#2335)
JasonOA888 Apr 18, 2026
1221448
fix(scripts): Cloud Provider Reports Security Issue(aliyun could) (#2…
airene Apr 18, 2026
aa6098e
chore(deps): bump langsmith from 0.6.4 to 0.7.31 in /backend (#2291)
dependabot[bot] Apr 18, 2026
be46635
chroe(script): disable the color log of langgraph
WillemJiang Apr 18, 2026
24fe5fb
fix(mcp): prevent RuntimeError from escaping except block in get_cach…
thresh111 Apr 18, 2026
5547401
fix(subagent): inherit parent agent's tool_groups in task_tool (#2305)
wisetwo Apr 18, 2026
5656f90
chore(deps-dev): bump pytest from 9.0.2 to 9.0.3 in /backend (#2349)
dependabot[bot] Apr 18, 2026
80e210f
[security] fix(uploads): require explicit opt-in for host-side docume…
Hinotoi-agent Apr 18, 2026
7c87dc5
fix(reasoning): prevent LLM-hallucinated HTML tags from rendering as …
xunliu Apr 19, 2026
f514e35
fix(backend): make clarification messages idempotent (#2350) (#2351)
ggnnggez Apr 19, 2026
a62ca5d
fix: Catch httpx.ReadError in the error handling (#2309)
xunliu Apr 19, 2026
05f1da0
fix(script): use portable locale for langgraph log pipeline on macOS …
YuanyuanMa03 Apr 19, 2026
c99865f
fix(token-usage): enable stream usage for openai-compatible models (#…
LittleChenLiya Apr 19, 2026
4be857f
fix: use Apple Container image pull syntax (#2366)
Kiteeater Apr 20, 2026
f2013f4
fix command palette hydration mismatch (#2301)
Eilen6316 Apr 20, 2026
fc94e90
fix(setup-agent): prevent data loss when setup fails on existing agen…
thresh111 Apr 20, 2026
6dce26a
fix: resolve tool duplication and skill parser YAML inconsistencies (…
asong56 Apr 20, 2026
ef04174
Fix invalid HTML nesting in reasoning trigger during complex task ren…
Copilot Apr 21, 2026
085c13e
fix: remove unnecessary f-string prefixes and unused import (#2352)
hummbl-dev Apr 21, 2026
5ba1dac
fix: rename present_file to present_files in docs and prompts (#2393)
wisetwo Apr 21, 2026
1ca2621
chore(deps): bump lxml from 6.0.2 to 6.1.0 in /backend (#2427)
dependabot[bot] Apr 22, 2026
dbd777f
chore(deps): bump python-dotenv from 1.2.1 to 1.2.2 in /backend (#2440)
dependabot[bot] Apr 22, 2026
c43c803
fix: remove mismatched context param in debug.py to suppress Pydantic…
whhe Apr 23, 2026
96d00f6
chore(deps): bump dompurify from 3.3.1 to 3.4.1 in /frontend (#2462)
dependabot[bot] Apr 23, 2026
b90f219
fix(skills): validate bundled SKILL.md front-matter in CI (fixes #244…
voidborne-d Apr 23, 2026
bd35cd3
chore(deps): bump uuid from 13.0.0 to 14.0.0 in /frontend (#2467)
dependabot[bot] Apr 23, 2026
c42ae3a
feat: add optional prompt-toolkit support to debug.py (#2461)
whhe Apr 23, 2026
4e72410
fix(gateway): bound lifespan shutdown hooks to prevent worker hang un…
JerryChaox Apr 23, 2026
30d619d
feat(subagents): support per-subagent skill loading and custom subage…
fancyboi999 Apr 23, 2026
cd12821
fix(backend): Updated the uv.lock with new added dependency
WillemJiang Apr 24, 2026
80a7446
fix(backend): fix the unit test error in backend
WillemJiang Apr 24, 2026
e8572b9
fix(jina): log transient failures at WARNING without traceback (#2484…
voidborne-d Apr 24, 2026
11f557a
feat(trace):Add run_name to the trace info for system agents. (#2492)
airene Apr 24, 2026
3a61126
fix: keep debug.py interactive terminal free from background log nois…
whhe Apr 24, 2026
c2332bb
fix memory settings layout overflow (#2420)
LittleChenLiya Apr 24, 2026
f9ff3a6
fix(middleware): avoid rescuing non-skill tool outputs during summari…
ggnnggez Apr 24, 2026
d78ed5c
fix: inherit subagent skill allowlists (#2514)
hetaoBackend Apr 24, 2026
ec8a8ca
fix: gate deferred MCP tool execution (#2513)
hetaoBackend Apr 24, 2026
b970993
fix: read lead agent options from context (#2515)
hetaoBackend Apr 24, 2026
2bb1a2d
feat(models): Provider for MindIE model engine (#2483)
pyp0327 Apr 25, 2026
950821c
fix: use subprocess instead of os.system in local_backend.py (#2494)
orbisai0security Apr 25, 2026
f394c0d
feat(mcp): support custom tool interceptors via extensions_config.jso…
IECspace Apr 25, 2026
1f59e94
fix: cap prompt caching breakpoints at 4 to prevent API 400 errors (#…
octo-patch Apr 25, 2026
410f0c4
fix(channels): accept single slack allowed user (#2481)
ming1523 Apr 25, 2026
8a04414
feat(dev): add pre-commit hooks for ruff, eslint, and prettier (#2525)
WillemJiang Apr 26, 2026
9dc2598
fix(channles):update the logger for the channel config (#2524)
WillemJiang Apr 26, 2026
af8c0cf
fix(harness): constrain view_image to thread data paths (#2557)
hetaoBackend Apr 28, 2026
f7dfb88
fix(aio-sandbox): redact env values in container logs (#2562)
hetaoBackend Apr 28, 2026
707ed32
fix(skills): scan skill archives before install (#2561)
hetaoBackend Apr 28, 2026
39c5da9
fix(sandbox): prevent local custom mount symlink escapes (#2558)
hetaoBackend Apr 28, 2026
6bd88fe
fix(sandbox): block host bash traversal escapes (#2560)
hetaoBackend Apr 28, 2026
395c143
chore(adpator):Adapt MindIE engine model and improve testing and fixe…
pyp0327 Apr 28, 2026
1168324
Merge upstream/main into argus (sync 2026-04-28)
Nichol4s Apr 28, 2026
e4457a8
fix(lint): satisfy ruff on argus patches after upstream merge
Nichol4s Apr 28, 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
7 changes: 1 addition & 6 deletions .github/workflows/argus-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,4 @@ jobs:

- name: Run unit tests
working-directory: backend
# test_artifacts_router.py expects lxml >= 6.1.0 behavior (xhtml is
# classified as active content). Our argus branch is pinned to
# upstream 898f4e8a, where uv.lock still has lxml 6.0.2; the bump
# to 6.1.0 lives in upstream commit 1ca2621 which we'll absorb on
# the first weekly sync. Drop --ignore once that merge lands.
run: PYTHONPATH=. uv run pytest tests/ -v --ignore=tests/test_artifacts_router.py
run: make test
63 changes: 63 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: E2E Tests

on:
push:
branches: [ 'main' ]
paths:
- 'frontend/**'
- '.github/workflows/e2e-tests.yml'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'frontend/**'
- '.github/workflows/e2e-tests.yml'

concurrency:
group: e2e-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
e2e-tests:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Enable Corepack
run: corepack enable

- name: Use pinned pnpm version
run: corepack prepare pnpm@10.26.2 --activate

- name: Install frontend dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile

- name: Install Playwright Chromium
working-directory: frontend
run: npx playwright install chromium --with-deps

- name: Run E2E tests
working-directory: frontend
run: pnpm exec playwright test
env:
SKIP_ENV_VALIDATION: '1'

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ coverage/
skills/custom/*
logs/
log/
debug.log

# Local git hooks (keep only on this machine, do not push)
.githooks/
Expand All @@ -55,5 +56,7 @@ web/
backend/Dockerfile.langgraph
config.yaml.bak
.playwright-mcp
/frontend/test-results/
/frontend/playwright-report/
.gstack/
.worktrees
33 changes: 33 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
repos:
# Backend: ruff lint + format via uv (uses the same ruff version as backend deps)
- repo: local
hooks:
- id: ruff
name: ruff lint
entry: bash -c 'cd backend && uv run ruff check --fix "${@/#backend\//}"' --
language: system
types_or: [python]
files: ^backend/
- id: ruff-format
name: ruff format
entry: bash -c 'cd backend && uv run ruff format "${@/#backend\//}"' --
language: system
types_or: [python]
files: ^backend/

# Frontend: eslint + prettier (must run from frontend/ for node_modules resolution)
- repo: local
hooks:
- id: frontend-eslint
name: eslint (frontend)
entry: bash -c 'cd frontend && npx eslint --fix "${@/#frontend\//}"' --
language: system
types_or: [javascript, tsx, ts]
files: ^frontend/

- id: frontend-prettier
name: prettier (frontend)
entry: bash -c 'cd frontend && npx prettier --write "${@/#frontend\//}"' --
language: system
files: ^frontend/
types_or: [javascript, tsx, ts, json, css]
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Required tools:

1. **Configure the application** (same as Docker setup above)

2. **Install dependencies**:
2. **Install dependencies** (this also sets up pre-commit hooks):
```bash
make install
```
Expand Down Expand Up @@ -300,9 +300,13 @@ Nginx (port 2026) ← Unified entry point
cd backend
make test

# Frontend tests
# Frontend unit tests
cd frontend
make test

# Frontend E2E tests (requires Chromium; builds and auto-starts the Next.js production server)
cd frontend
make test-e2e
```

### PR Regression Checks
Expand All @@ -311,6 +315,7 @@ Every pull request triggers the following CI workflows:

- **Backend unit tests** — [.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml)
- **Frontend unit tests** — [.github/workflows/frontend-unit-tests.yml](.github/workflows/frontend-unit-tests.yml)
- **Frontend E2E tests** — [.github/workflows/e2e-tests.yml](.github/workflows/e2e-tests.yml) (triggered only when `frontend/` files change)

## Code Style

Expand Down
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ help:
@echo " make config - Generate local config files (aborts if config already exists)"
@echo " make config-upgrade - Merge new fields from config.example.yaml into config.yaml"
@echo " make check - Check if all required tools are installed"
@echo " make install - Install all dependencies (frontend + backend)"
@echo " make install - Install all dependencies (frontend + backend + pre-commit hooks)"
@echo " make setup-sandbox - Pre-pull sandbox container image (recommended)"
@echo " make dev - Start all services in development mode (with hot-reloading)"
@echo " make dev-pro - Start in dev + Gateway mode (experimental, no LangGraph server)"
Expand Down Expand Up @@ -73,6 +73,8 @@ install:
@cd backend && uv sync
@echo "Installing frontend dependencies..."
@cd frontend && pnpm install
@echo "Installing pre-commit hooks..."
@$(BACKEND_UV_RUN) --with pre-commit pre-commit install
@echo "✓ All dependencies installed"
@echo ""
@echo "=========================================="
Expand All @@ -99,7 +101,7 @@ setup-sandbox:
echo ""; \
if command -v container >/dev/null 2>&1 && [ "$$(uname)" = "Darwin" ]; then \
echo "Detected Apple Container on macOS, pulling image..."; \
container pull "$$IMAGE" || echo "⚠ Apple Container pull failed, will try Docker"; \
container image pull "$$IMAGE" || echo "⚠ Apple Container pull failed, will try Docker"; \
fi; \
if command -v docker >/dev/null 2>&1; then \
echo "Pulling image using Docker..."; \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ On Windows, run the local development flow from Git Bash. Native `cmd.exe` and P

2. **Install dependencies**:
```bash
make install # Install backend + frontend dependencies
make install # Install backend + frontend dependencies + pre-commit hooks
```

3. **(Optional) Pre-pull sandbox image**:
Expand Down
21 changes: 20 additions & 1 deletion backend/app/channels/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
"wecom": "app.channels.wecom:WeComChannel",
}

# Keys that indicate a user has configured credentials for a channel.
_CHANNEL_CREDENTIAL_KEYS: dict[str, list[str]] = {
"discord": ["bot_token"],
"feishu": ["app_id", "app_secret"],
"slack": ["bot_token", "app_token"],
"telegram": ["bot_token"],
"wecom": ["bot_id", "bot_secret"],
"wechat": ["bot_token"],
}

_CHANNELS_LANGGRAPH_URL_ENV = "DEER_FLOW_CHANNELS_LANGGRAPH_URL"
_CHANNELS_GATEWAY_URL_ENV = "DEER_FLOW_CHANNELS_GATEWAY_URL"

Expand Down Expand Up @@ -88,7 +98,16 @@ async def start(self) -> None:
if not isinstance(channel_config, dict):
continue
if not channel_config.get("enabled", False):
logger.info("Channel %s is disabled, skipping", name)
cred_keys = _CHANNEL_CREDENTIAL_KEYS.get(name, [])
has_creds = any(not isinstance(channel_config.get(k), bool) and channel_config.get(k) is not None and str(channel_config[k]).strip() for k in cred_keys)
if has_creds:
logger.warning(
"Channel '%s' has credentials configured but is disabled. Set enabled: true under channels.%s in config.yaml to activate it.",
name,
name,
)
else:
logger.info("Channel %s is disabled, skipping", name)
continue

await self._start_channel(name, channel_config)
Expand Down
22 changes: 20 additions & 2 deletions backend/app/channels/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,39 @@
_slack_md_converter = SlackMarkdownConverter()


def _normalize_allowed_users(allowed_users: Any) -> set[str]:
if allowed_users is None:
return set()
if isinstance(allowed_users, str):
values = [allowed_users]
elif isinstance(allowed_users, list | tuple | set):
values = allowed_users
else:
logger.warning(
"Slack allowed_users should be a list of Slack user IDs or a single Slack user ID string; treating %s as one string value",
type(allowed_users).__name__,
)
values = [allowed_users]
return {str(user_id) for user_id in values if str(user_id)}


class SlackChannel(Channel):
"""Slack IM channel using Socket Mode (WebSocket, no public IP).

Configuration keys (in ``config.yaml`` under ``channels.slack``):
- ``bot_token``: Slack Bot User OAuth Token (xoxb-...).
- ``app_token``: Slack App-Level Token (xapp-...) for Socket Mode.
- ``allowed_users``: (optional) List of allowed Slack user IDs. Empty = allow all.
- ``allowed_users``: (optional) List of allowed Slack user IDs, or a
single Slack user ID string as shorthand. Empty = allow all. Other
scalar values are treated as a single string with a warning.
"""

def __init__(self, bus: MessageBus, config: dict[str, Any]) -> None:
super().__init__(name="slack", bus=bus, config=config)
self._socket_client = None
self._web_client = None
self._loop: asyncio.AbstractEventLoop | None = None
self._allowed_users: set[str] = {str(user_id) for user_id in config.get("allowed_users", [])}
self._allowed_users = _normalize_allowed_users(config.get("allowed_users", []))

async def start(self) -> None:
if self._running:
Expand Down
18 changes: 16 additions & 2 deletions backend/app/gateway/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
Expand Down Expand Up @@ -32,6 +33,11 @@

logger = logging.getLogger(__name__)

# Upper bound (seconds) each lifespan shutdown hook is allowed to run.
# Bounds worker exit time so uvicorn's reload supervisor does not keep
# firing signals into a worker that is stuck waiting for shutdown cleanup.
_SHUTDOWN_HOOK_TIMEOUT_SECONDS = 5.0


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
Expand Down Expand Up @@ -63,11 +69,19 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:

yield

# Stop channel service on shutdown
# Stop channel service on shutdown (bounded to prevent worker hang)
try:
from app.channels.service import stop_channel_service

await stop_channel_service()
await asyncio.wait_for(
stop_channel_service(),
timeout=_SHUTDOWN_HOOK_TIMEOUT_SECONDS,
)
except TimeoutError:
logger.warning(
"Channel service shutdown exceeded %.1fs; proceeding with worker exit.",
_SHUTDOWN_HOOK_TIMEOUT_SECONDS,
)
except Exception:
logger.exception("Failed to stop channel service")

Expand Down
25 changes: 21 additions & 4 deletions backend/app/gateway/routers/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AgentResponse(BaseModel):
description: str = Field(default="", description="Agent description")
model: str | None = Field(default=None, description="Optional model override")
tool_groups: list[str] | None = Field(default=None, description="Optional tool group whitelist")
skills: list[str] | None = Field(default=None, description="Optional skill whitelist (None=all, []=none)")
soul: str | None = Field(default=None, description="SOUL.md content")


Expand All @@ -41,6 +42,7 @@ class AgentCreateRequest(BaseModel):
description: str = Field(default="", description="Agent description")
model: str | None = Field(default=None, description="Optional model override")
tool_groups: list[str] | None = Field(default=None, description="Optional tool group whitelist")
skills: list[str] | None = Field(default=None, description="Optional skill whitelist (None=all enabled, []=none)")
soul: str = Field(default="", description="SOUL.md content — agent personality and behavioral guardrails")


Expand All @@ -50,6 +52,7 @@ class AgentUpdateRequest(BaseModel):
description: str | None = Field(default=None, description="Updated description")
model: str | None = Field(default=None, description="Updated model override")
tool_groups: list[str] | None = Field(default=None, description="Updated tool group whitelist")
skills: list[str] | None = Field(default=None, description="Updated skill whitelist (None=all, []=none)")
soul: str | None = Field(default=None, description="Updated SOUL.md content")


Expand Down Expand Up @@ -94,6 +97,7 @@ def _agent_config_to_response(agent_cfg: AgentConfig, include_soul: bool = False
description=agent_cfg.description,
model=agent_cfg.model,
tool_groups=agent_cfg.tool_groups,
skills=agent_cfg.skills,
soul=soul,
)

Expand Down Expand Up @@ -215,6 +219,8 @@ async def create_agent_endpoint(request: AgentCreateRequest) -> AgentResponse:
config_data["model"] = request.model
if request.tool_groups is not None:
config_data["tool_groups"] = request.tool_groups
if request.skills is not None:
config_data["skills"] = request.skills

config_file = agent_dir / "config.yaml"
with open(config_file, "w", encoding="utf-8") as f:
Expand Down Expand Up @@ -271,21 +277,32 @@ async def update_agent(name: str, request: AgentUpdateRequest) -> AgentResponse:

try:
# Update config if any config fields changed
config_changed = any(v is not None for v in [request.description, request.model, request.tool_groups])
# Use model_fields_set to distinguish "field omitted" from "explicitly set to null".
# This is critical for skills where None means "inherit all" (not "don't change").
fields_set = request.model_fields_set
config_changed = bool(fields_set & {"description", "model", "tool_groups", "skills"})

if config_changed:
updated: dict = {
"name": agent_cfg.name,
"description": request.description if request.description is not None else agent_cfg.description,
"description": request.description if "description" in fields_set else agent_cfg.description,
}
new_model = request.model if request.model is not None else agent_cfg.model
new_model = request.model if "model" in fields_set else agent_cfg.model
if new_model is not None:
updated["model"] = new_model

new_tool_groups = request.tool_groups if request.tool_groups is not None else agent_cfg.tool_groups
new_tool_groups = request.tool_groups if "tool_groups" in fields_set else agent_cfg.tool_groups
if new_tool_groups is not None:
updated["tool_groups"] = new_tool_groups

# skills: None = inherit all, [] = no skills, ["a","b"] = whitelist
if "skills" in fields_set:
new_skills = request.skills
else:
new_skills = agent_cfg.skills
if new_skills is not None:
updated["skills"] = new_skills

config_file = agent_dir / "config.yaml"
with open(config_file, "w", encoding="utf-8") as f:
yaml.dump(updated, f, default_flow_style=False, allow_unicode=True)
Expand Down
Loading
Loading