diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index c74d6c8b..f1deaa71 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -53,6 +53,8 @@ packages = ["src"] [tool.ruff] target-version = "py311" line-length = 88 + +[tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings @@ -66,7 +68,7 @@ ignore = [ "E501", # line too long (handled by formatter) ] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["src"] [tool.mypy] diff --git a/apps/api/scripts/tracer_bullet_2.py b/apps/api/scripts/tracer_bullet_2.py index f2e30559..7a1d3e69 100644 --- a/apps/api/scripts/tracer_bullet_2.py +++ b/apps/api/scripts/tracer_bullet_2.py @@ -326,7 +326,7 @@ class TracerBullet2: approval_result = await self.step4_multisig_approval(approval_card) # Step 5: MCP Execution - execution_result = await self.step5_mcp_execution(approval_result, approval_card) + _execution_result = await self.step5_mcp_execution(approval_result, approval_card) # Summary print("\n" + "=" * 60) diff --git a/apps/api/src/api/v1/ai.py b/apps/api/src/api/v1/ai.py index 3512fcd3..183adfc4 100644 --- a/apps/api/src/api/v1/ai.py +++ b/apps/api/src/api/v1/ai.py @@ -169,7 +169,7 @@ async def analyze_and_propose( raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"Failed to fetch monitoring data: {str(e)}", - ) + ) from e # Step 2: 呼叫 OpenClaw AI try: @@ -190,7 +190,7 @@ async def analyze_and_propose( raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"AI analysis failed: {str(e)}", - ) + ) from e # Step 3: 處理決策 if decision is None: diff --git a/apps/api/src/api/v1/audit_logs.py b/apps/api/src/api/v1/audit_logs.py index 63ac9e3b..5392df54 100644 --- a/apps/api/src/api/v1/audit_logs.py +++ b/apps/api/src/api/v1/audit_logs.py @@ -198,7 +198,7 @@ async def get_audit_stats() -> AuditStatsResponse: # Success/Failure count success_result = await db.execute( - select(func.count(AuditLog.id)).where(AuditLog.success == True) + select(func.count(AuditLog.id)).where(AuditLog.success is True) ) success_count = success_result.scalar() or 0 failure_count = total_count - success_count diff --git a/apps/api/src/api/v1/incidents.py b/apps/api/src/api/v1/incidents.py index 3e9701d8..004a5396 100644 --- a/apps/api/src/api/v1/incidents.py +++ b/apps/api/src/api/v1/incidents.py @@ -205,7 +205,7 @@ async def list_incidents() -> IncidentListResponse: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to list incidents: {str(e)}", - ) + ) from e # ============================================================================= @@ -263,7 +263,7 @@ async def get_incident(incident_id: str) -> IncidentResponse: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get incident: {str(e)}", - ) + ) from e # ============================================================================= diff --git a/apps/api/src/api/v1/proposals.py b/apps/api/src/api/v1/proposals.py index 646ae83a..a95e9856 100644 --- a/apps/api/src/api/v1/proposals.py +++ b/apps/api/src/api/v1/proposals.py @@ -23,6 +23,7 @@ Date: 2026-03-23 """ from datetime import datetime +from typing import Annotated from uuid import UUID from fastapi import APIRouter, HTTPException, Query, status @@ -228,7 +229,7 @@ async def create_proposal( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal Error: {str(e)}", - ) + ) from e # ============================================================================= @@ -242,17 +243,19 @@ async def create_proposal( description="取得所有提案,可依狀態篩選。", ) async def list_proposals( - status_filter: ApprovalStatus | None = Query( - None, - alias="status", - description="篩選狀態 (pending/approved/rejected/expired)", - ), - incident_id: str | None = Query( - None, - description="篩選特定 Incident 的提案", - ), - limit: int = Query(50, ge=1, le=200, description="每頁數量"), - offset: int = Query(0, ge=0, description="偏移量"), + status_filter: Annotated[ + ApprovalStatus | None, + Query( + alias="status", + description="篩選狀態 (pending/approved/rejected/expired)", + ), + ] = None, + incident_id: Annotated[ + str | None, + Query(description="篩選特定 Incident 的提案"), + ] = None, + limit: Annotated[int, Query(ge=1, le=200, description="每頁數量")] = 50, + offset: Annotated[int, Query(ge=0, description="偏移量")] = 0, ) -> ProposalListResponse: """ 查詢提案清單 @@ -308,7 +311,7 @@ async def list_proposals( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to list proposals: {str(e)}", - ) + ) from e # ============================================================================= @@ -342,11 +345,11 @@ async def get_proposal( # 驗證 UUID 格式 try: uuid = UUID(proposal_id) - except ValueError: + except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid proposal ID format: {proposal_id}", - ) + ) from e approval = await approval_service.get_approval_by_id(uuid) @@ -375,7 +378,7 @@ async def get_proposal( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get proposal: {str(e)}", - ) + ) from e # ============================================================================= @@ -420,11 +423,11 @@ async def approve_proposal( # 驗證 UUID 格式 try: uuid = UUID(proposal_id) - except ValueError: + except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid proposal ID format: {proposal_id}", - ) + ) from e # 取得現有提案 approval = await approval_service.get_approval_by_id(uuid) @@ -494,4 +497,4 @@ async def approve_proposal( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to approve proposal: {str(e)}", - ) + ) from e diff --git a/apps/api/src/api/v1/telegram.py b/apps/api/src/api/v1/telegram.py index 126a3702..9c7a2416 100644 --- a/apps/api/src/api/v1/telegram.py +++ b/apps/api/src/api/v1/telegram.py @@ -131,7 +131,7 @@ async def telegram_webhook( # ===================================================================== action = result["action"] approval_id = result["approval_id"] - telegram_user = result["user"] + _telegram_user = result["user"] # reserved for future audit logging service = get_approval_service() @@ -245,7 +245,7 @@ async def test_push( raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Telegram API error: {str(e)}", - ) + ) from e @router.get( diff --git a/apps/api/src/api/v1/webhooks.py b/apps/api/src/api/v1/webhooks.py index 240d298e..8233b4a4 100644 --- a/apps/api/src/api/v1/webhooks.py +++ b/apps/api/src/api/v1/webhooks.py @@ -522,7 +522,7 @@ async def receive_signal( raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"HMAC verification failed: {str(e)}", - ) + ) from e try: # 寫入 Redis Stream @@ -539,7 +539,7 @@ async def receive_signal( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to produce signal: {str(e)}", - ) + ) from e # ============================================================================= @@ -719,7 +719,7 @@ async def receive_alert( raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"HMAC verification failed: {str(e)}", - ) + ) from e alert_id = f"alert-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" @@ -974,7 +974,7 @@ async def receive_alert( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"告警處理失敗: {str(e)}", - ) + ) from e @router.get( diff --git a/apps/api/src/core/trust_engine.py b/apps/api/src/core/trust_engine.py index d5eaf391..87c2f826 100644 --- a/apps/api/src/core/trust_engine.py +++ b/apps/api/src/core/trust_engine.py @@ -316,11 +316,11 @@ class TrustEngine: approval.updated_at = datetime.now(UTC) # 檢查是否滿足簽核數 - execution_triggered = False + _execution_triggered = False if approval.is_fully_signed: approval.status = ApprovalStatus.APPROVED approval.resolved_at = datetime.now(UTC) - execution_triggered = True + _execution_triggered = True if self._on_approved: self._on_approved(approval) diff --git a/apps/api/src/routers/proposals.py b/apps/api/src/routers/proposals.py index aa6b2ba2..4a5426b9 100644 --- a/apps/api/src/routers/proposals.py +++ b/apps/api/src/routers/proposals.py @@ -62,7 +62,7 @@ def get_real_proposal_service() -> ProposalService: async def generate_decision_proposal( incident_id: str, request: ProposalCreateRequest, - service: ProposalService = Depends(get_real_proposal_service), + service: ProposalService = Depends(get_real_proposal_service), # noqa: B008 ): """ Phase 6.4h: 真實 LLM 決策提案生成 @@ -157,4 +157,4 @@ async def generate_decision_proposal( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal Error: {str(e)}" - ) + ) from e diff --git a/apps/api/src/routes/approvals.py b/apps/api/src/routes/approvals.py index caf6a807..1af31952 100644 --- a/apps/api/src/routes/approvals.py +++ b/apps/api/src/routes/approvals.py @@ -235,7 +235,7 @@ async def approve_approval( "role": e.role, "required_roles": e.required_roles, }, - ) + ) from e except DuplicateSignatureError as e: raise HTTPException( @@ -244,13 +244,13 @@ async def approve_approval( "error": "Duplicate signature", "user_id": e.user_id, }, - ) + ) from e except ApprovalAlreadyDecidedError as e: raise HTTPException( status_code=400, detail={"error": str(e)}, - ) + ) from e except TOCTOUConflictError as e: # ⚠️ TOCTOU 衝突 - 資源狀態已改變 @@ -262,7 +262,7 @@ async def approve_approval( "failed_checks": e.failed_checks, "signatures_cleared": True, }, - ) + ) from e @router.post("/{approval_id}/reject", response_model=Approval) diff --git a/apps/api/src/services/approval.py b/apps/api/src/services/approval.py index 08e60e42..e3b667b2 100644 --- a/apps/api/src/services/approval.py +++ b/apps/api/src/services/approval.py @@ -238,10 +238,10 @@ class MultiSigEngine: if isinstance(user_role, str): try: user_role = UserRole(user_role.lower()) - except ValueError: + except ValueError as e: raise InsufficientPermissionError( user_role, [r.value for r in RISK_MATRIX[state.risk_level].allowed_roles] - ) + ) from e # 4. 檢查角色是否有權簽章 requirement = RISK_MATRIX[state.risk_level] diff --git a/apps/api/src/services/consensus_engine.py b/apps/api/src/services/consensus_engine.py index 490b90d0..5626acf4 100644 --- a/apps/api/src/services/consensus_engine.py +++ b/apps/api/src/services/consensus_engine.py @@ -236,7 +236,7 @@ class SecurityAgent(ExpertAgent): async def analyze(self, incident: Incident) -> AgentOpinion: """資安視角分析""" - target = incident.affected_services[0] if incident.affected_services else "unknown" + _target = incident.affected_services[0] if incident.affected_services else "unknown" alert_names = " ".join([s.alert_name.lower() for s in incident.signals]) # 資安掃描 diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py index 8a724cda..999d12c3 100644 --- a/apps/api/src/services/decision_manager.py +++ b/apps/api/src/services/decision_manager.py @@ -296,7 +296,7 @@ class DecisionManager: 這個方法保證在 timeout_sec 內返回有效 token """ - redis_client = get_redis() + _redis_client = get_redis() # 1. 檢查現有 token existing_token = await self._find_existing_token(incident.incident_id) diff --git a/apps/api/src/services/executor.py b/apps/api/src/services/executor.py index c5dd4f03..677fb893 100644 --- a/apps/api/src/services/executor.py +++ b/apps/api/src/services/executor.py @@ -1011,7 +1011,7 @@ async def execute_approved_proposal(approval_id: str) -> ExecutionResult: import json try: metadata = json.loads(metadata) - except: + except Exception: metadata = {} kubectl_command = metadata.get("kubectl_command", "") diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 119ca19e..3ce2726d 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -307,11 +307,11 @@ class TelegramGateway: except httpx.HTTPStatusError as e: logger.error("telegram_api_error", method=method, status=e.response.status_code) - raise TelegramGatewayError(f"HTTP error: {e.response.status_code}") + raise TelegramGatewayError(f"HTTP error: {e.response.status_code}") from e except Exception as e: logger.error("telegram_request_failed", method=method, error=str(e)) - raise TelegramGatewayError(str(e)) + raise TelegramGatewayError(str(e)) from e def _build_inline_keyboard( self, diff --git a/apps/api/src/services/test_context_gatherer.py b/apps/api/src/services/test_context_gatherer.py index 08040800..4189658e 100644 --- a/apps/api/src/services/test_context_gatherer.py +++ b/apps/api/src/services/test_context_gatherer.py @@ -130,7 +130,7 @@ Traceback (most recent call last): """.strip() filtered = LogLevelFilter.filter_logs(raw_logs) - lines = [l for l in filtered.split("\n") if l.strip()] + _lines = [line for line in filtered.split("\n") if line.strip()] # 驗證: 只有 ERROR/WARN/CRITICAL 和 Stacktrace 被保留 assert "[INFO]" not in filtered, "INFO should be filtered" diff --git a/apps/api/src/workers/signal_worker.py b/apps/api/src/workers/signal_worker.py index 4655dacc..236abafd 100644 --- a/apps/api/src/workers/signal_worker.py +++ b/apps/api/src/workers/signal_worker.py @@ -173,7 +173,7 @@ class SignalWorker: continue # messages 格式: [[stream_name, [(id, data), ...]]] - for stream_name, entries in messages: + for _stream_name, entries in messages: for message_id, data in entries: await self._process_signal(message_id, data) diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 740f5e0d..eba17a16 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -14,15 +14,17 @@ module.exports = { // Next.js specific '@next/next/no-html-link-for-pages': 'off', - // Allow console in development - 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + // Allow console for debugging (TODO: integrate unified logger) + 'no-console': 'off', // i18n enforcement - no hardcoded strings in JSX // (Custom rule would require eslint-plugin-i18n-json setup) // TypeScript strict rules '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + '@typescript-eslint/consistent-type-imports': 'warn', + 'no-constant-condition': 'warn', }, ignorePatterns: [ 'node_modules', diff --git a/apps/web/src/app/[locale]/action-logs/page.tsx b/apps/web/src/app/[locale]/action-logs/page.tsx index db313bad..f623e698 100644 --- a/apps/web/src/app/[locale]/action-logs/page.tsx +++ b/apps/web/src/app/[locale]/action-logs/page.tsx @@ -18,7 +18,7 @@ import { useState, useEffect, useCallback } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' -import { DataPincerPanel, DataPincerCard } from '@/components/cyber' +import { DataPincerPanel } from '@/components/cyber' import { cn } from '@/lib/utils' import { FileText, @@ -82,6 +82,7 @@ const getApiBaseUrl = (): string => { // 統帥鐵律: 禁止任何 Fallback IP const url = process.env.NEXT_PUBLIC_API_URL if (!url) { + // eslint-disable-next-line no-console console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') return '' } @@ -198,7 +199,7 @@ export default function ActionLogPage({ } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' setError(message) - console.error('[ActionLog] Fetch error:', message) + console.error('[ActionLog] Fetch error:', message) // eslint-disable-line no-console } finally { setIsLoading(false) } @@ -221,7 +222,7 @@ export default function ActionLogPage({ setStats(data) } } catch (err) { - console.error('[ActionLog] Stats fetch error:', err) + console.error('[ActionLog] Stats fetch error:', err) // eslint-disable-line no-console } }, []) diff --git a/apps/web/src/app/[locale]/demo/page.tsx b/apps/web/src/app/[locale]/demo/page.tsx index 3a08fc4f..7c116ed6 100644 --- a/apps/web/src/app/[locale]/demo/page.tsx +++ b/apps/web/src/app/[locale]/demo/page.tsx @@ -25,7 +25,7 @@ const getApiBaseUrl = (): string => { if (typeof window === 'undefined') return '' const url = process.env.NEXT_PUBLIC_API_URL if (!url) { - console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') + console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') // eslint-disable-line no-console return '' } return url @@ -134,10 +134,10 @@ export default function DemoPage({ params }: { params: { locale: string } }) { setCreateError(null) try { const result = await createTestApprovalWithConfig(riskLevel, approvalConfigs[riskLevel]) - console.log('[Demo] Created approval:', result) + console.log('[Demo] Created approval:', result) // eslint-disable-line no-console } catch (err) { setCreateError(`${tApproval('fetchError')}: ${err}`) - console.error('[Demo] Create approval failed:', err) + console.error('[Demo] Create approval failed:', err) // eslint-disable-line no-console } finally { setIsCreating(false) } diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index e5f5cd3b..c6aa0dab 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -25,7 +25,7 @@ import { OpenClawStateMachine } from '@/components/ai/openclaw-state-machine' import { GlobalPulseChart } from '@/components/charts/global-pulse-chart' import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics' import { useIncidents } from '@/hooks/useIncidents' -import { DecisionInfo } from '@/lib/api-client' +import type { DecisionInfo } from '@/lib/api-client' import { IncidentCard, IncidentCardGrid, diff --git a/apps/web/src/components/incident/dual-state-incident-card.tsx b/apps/web/src/components/incident/dual-state-incident-card.tsx index 26854139..270650e6 100644 --- a/apps/web/src/components/incident/dual-state-incident-card.tsx +++ b/apps/web/src/components/incident/dual-state-incident-card.tsx @@ -29,7 +29,8 @@ import React, { useState, useCallback, useEffect, useRef } from 'react' import { useTranslations } from 'next-intl' -import { apiClient, DecisionInfo } from '@/lib/api-client' +import type { DecisionInfo } from '@/lib/api-client'; +import { apiClient } from '@/lib/api-client' type ButtonState = 'idle' | 'loading' | 'approved' | 'rejected' | 'error' | 'timeout' diff --git a/apps/web/src/components/shared/auto-healing-error-boundary.tsx b/apps/web/src/components/shared/auto-healing-error-boundary.tsx index 7c552d86..1c08596a 100644 --- a/apps/web/src/components/shared/auto-healing-error-boundary.tsx +++ b/apps/web/src/components/shared/auto-healing-error-boundary.tsx @@ -1,6 +1,8 @@ 'use client' +/* eslint-disable no-console -- Error boundary requires console for crash logging */ -import React, { Component, ErrorInfo, ReactNode } from 'react' +import type { ErrorInfo, ReactNode } from 'react'; +import React, { Component } from 'react' import { useTranslations } from 'next-intl' import { useAgentStore } from '@/stores/agent.store' diff --git a/packages/lewooogo-core/.eslintrc.js b/packages/lewooogo-core/.eslintrc.js new file mode 100644 index 00000000..0beeabfa --- /dev/null +++ b/packages/lewooogo-core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['@awoooi/eslint-config/base'], + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, +}