Harden supplemental security boundaries
This commit is contained in:
1 parent
7e167ea238
commit
052277bb56
15 files changed
+362
-53
No files matched your search
@@ -51,13 +51,14 @@
|
||||
|
||||
## 📰 News
|
||||
|
||||
- **2026-05-05** 🛡️ **Security boundary follow-up**: Completes the remaining security-boundary hardening around explicit CORS origins, Settings credential indicators, web URL reading, and Shadow Account code generation, with regression tests added for each path. Normal localhost CLI/Web UI workflows stay the same; remote deployments should continue using `API_AUTH_KEY` and explicit trusted origins.
|
||||
- **2026-05-04** 🖥️ **Interactive CLI UX + CI cleanup**: Interactive mode now has a live bottom status bar showing provider/model, session duration, last-run latency, and cumulative tool-call stats, plus prompt history navigation and cursor editing with arrow keys via `prompt_toolkit` ([#69](https://github.com/HKUDS/Vibe-Trading/pull/69)). The CLI still falls back to Rich prompts when `prompt_toolkit` or a TTY is unavailable. CI path expectations were also aligned with the hardened file-import sandbox and cross-platform `/tmp` resolution, returning main to green ([`bb67dc7`](https://github.com/HKUDS/Vibe-Trading/commit/bb67dc7cfcc11553c57d8962bee56381dca43758)).
|
||||
- **2026-05-03** 🛡️ **Security hardening patch**: Tightens default API authentication for non-local deployments, protects sensitive run/session/swarm reads, restricts upload and local file-reading boundaries, gates shell-capable tools by entry point, validates generated strategy loading before import, and runs the Docker image as a non-root user with a localhost-only published port by default. Local CLI and localhost Web UI workflows remain low-friction; remote API/Web deployments should set `API_AUTH_KEY`.
|
||||
- **2026-05-02** 🧭 **Dividend analysis + sharper roadmap**: Added the `dividend-analysis` skill for income stocks, payout sustainability, dividend growth, shareholder yield, ex-dividend mechanics, and yield-trap checks, pinned by bundled-skill regression tests. The public roadmap now focuses on upcoming work: Research Autopilot, Data Bridge, Options Lab, Portfolio Studio, Alpha Zoo, Research Delivery, Trust Layer, and Community sharing.
|
||||
|
||||
<details>
|
||||
<summary>Earlier news</summary>
|
||||
|
||||
- **2026-05-02** 🧭 **Dividend analysis + sharper roadmap**: Added the `dividend-analysis` skill for income stocks, payout sustainability, dividend growth, shareholder yield, ex-dividend mechanics, and yield-trap checks, pinned by bundled-skill regression tests. The public roadmap now focuses on upcoming work: Research Autopilot, Data Bridge, Options Lab, Portfolio Studio, Alpha Zoo, Research Delivery, Trust Layer, and Community sharing.
|
||||
- **2026-05-01** 🔥 **Correlation heatmap + OpenAI Codex OAuth + A-share pre-ST filter**: New correlation dashboard/API computes rolling return correlations and renders an ECharts heatmap for portfolio and symbol analysis ([#64](https://github.com/HKUDS/Vibe-Trading/pull/64)). OpenAI Codex provider support now uses ChatGPT OAuth via `vibe-trading provider login openai-codex`, with Settings metadata and adapter regression tests ([#65](https://github.com/HKUDS/Vibe-Trading/pull/65)). Added and hardened the `ashare-pre-st-filter` skill for A-share ST/*ST risk screening, including Sina penalty relevance filtering so securities-account mentions do not inflate E2 counts ([#63](https://github.com/HKUDS/Vibe-Trading/pull/63)).
|
||||
- **2026-04-30** ⚙️ **Web UI Settings + validation CLI hardening**: New Settings page for LLM provider/model, base URL, reasoning effort, and data source credentials, backed by local/auth-protected settings APIs and data-driven provider metadata ([#57](https://github.com/HKUDS/Vibe-Trading/pull/57)). Also hardens `python -m backtest.validation <run_dir>` so missing, blank, malformed, non-existent, and non-directory inputs fail with clear operator-facing messages before validation starts ([#60](https://github.com/HKUDS/Vibe-Trading/pull/60)).
|
||||
- **2026-04-28** 🚀 **v0.1.6 released** (`pip install -U vibe-trading-ai`): Fixes `vibe-trading --swarm-presets` returning empty after `pip install` / `uv tool install` ([#55](https://github.com/HKUDS/Vibe-Trading/issues/55)) — preset YAMLs now bundled inside the `src.swarm` package and pinned by a 6-test regression suite. Plus AKShare loader correctly routes ETFs (`510300.SH`) and forex (`USDCNH`) to the right endpoints with hardened registry fallback. Rolls up everything since v0.1.5: benchmark comparison panel, `/upload` streaming + size limits, Futu loader (HK + A-share), vnpy export skill, security hardening, frontend lazy loading (688KB → 262KB).
|
||||
|
||||
+2
-1
@@ -51,13 +51,14 @@
|
||||
|
||||
## 📰 أحدث الأخبار
|
||||
|
||||
- **2026-05-05** 🛡️ **متابعة لتعزيز الحدود الأمنية**: يستكمل تعزيز الحدود الأمنية حول origins الصريحة في CORS، ومؤشرات بيانات الاعتماد في Settings، وقراءة عناوين URL على الويب، وتوليد كود Shadow Account، مع إضافة اختبارات انحدار لكل مسار. تبقى تدفقات CLI وWeb UI على localhost كما هي؛ ويجب أن تستمر عمليات النشر البعيدة في استخدام `API_AUTH_KEY` وorigins موثوقة صريحة.
|
||||
- **2026-05-04** 🖥️ **تحسين تجربة CLI التفاعلية + تنظيف CI**: أصبح الوضع التفاعلي يعرض شريط حالة سفلياً مباشراً يوضح provider/model ومدة الجلسة ووقت آخر تشغيل وإحصاءات استدعاءات الأدوات التراكمية، مع دعم تصفح السجل وتحرير المؤشر بأسهم لوحة المفاتيح عبر `prompt_toolkit` ([#69](https://github.com/HKUDS/Vibe-Trading/pull/69)). وعند غياب `prompt_toolkit` أو TTY، يعود CLI إلى Rich prompts. كما تمت مواءمة توقعات مسارات CI مع صندوق استيراد الملفات المعزز وحل `/tmp` عبر المنصات، فعاد main إلى الحالة الخضراء ([`bb67dc7`](https://github.com/HKUDS/Vibe-Trading/commit/bb67dc7cfcc11553c57d8962bee56381dca43758)).
|
||||
- **2026-05-03** 🛡️ **تصحيح لتعزيز الأمان**: يشدد المصادقة الافتراضية للـ API في النشر غير المحلي، ويحمي قراءات run/session/swarm الحساسة، ويقيّد حدود الرفع وقراءة الملفات المحلية، ويتحكم في أدوات shell بحسب نقطة الدخول، ويتحقق من الاستراتيجيات المولدة قبل الاستيراد، ويجعل صورة Docker تعمل افتراضياً كمستخدم غير root وبمنفذ منشور على localhost فقط. تبقى تجربة CLI وWeb UI المحلي منخفضة الاحتكاك؛ ويجب على نشر API/Web البعيد ضبط `API_AUTH_KEY`.
|
||||
- **2026-05-02** 🧭 **تحليل التوزيعات + خارطة طريق أوضح**: تمت إضافة مهارة `dividend-analysis` لأسهم الدخل، واستدامة التوزيعات، ونمو التوزيعات، وعائد المساهمين، وآليات تاريخ الاستحقاق السابق، وفحص فخاخ العائد المرتفع، مع تثبيتها باختبارات انحدار للمهارات المضمنة. تركز خارطة الطريق العامة الآن على Research Autopilot وData Bridge وOptions Lab وPortfolio Studio وAlpha Zoo وResearch Delivery وTrust Layer ومشاركة Community.
|
||||
|
||||
<details>
|
||||
<summary>أخبار سابقة</summary>
|
||||
|
||||
- **2026-05-02** 🧭 **تحليل التوزيعات + خارطة طريق أوضح**: تمت إضافة مهارة `dividend-analysis` لأسهم الدخل، واستدامة التوزيعات، ونمو التوزيعات، وعائد المساهمين، وآليات تاريخ الاستحقاق السابق، وفحص فخاخ العائد المرتفع، مع تثبيتها باختبارات انحدار للمهارات المضمنة. تركز خارطة الطريق العامة الآن على Research Autopilot وData Bridge وOptions Lab وPortfolio Studio وAlpha Zoo وResearch Delivery وTrust Layer ومشاركة Community.
|
||||
- **2026-05-01** 🔥 **خريطة ارتباط حرارية + OpenAI Codex OAuth + مرشح pre-ST لأسهم A**: لوحة/API ارتباط جديدة تحسب ارتباطات العوائد المتحركة وتعرضها كخريطة حرارية ECharts لتحليل المحافظ والرموز ([#64](https://github.com/HKUDS/Vibe-Trading/pull/64)). مزود OpenAI Codex يدعم الآن ChatGPT OAuth عبر `vibe-trading provider login openai-codex`، مع بيانات Settings واختبارات انحدار للمحوّل ([#65](https://github.com/HKUDS/Vibe-Trading/pull/65)). تمت إضافة وتعزيز مهارة `ashare-pre-st-filter` لفحص مخاطر ST/*ST في أسهم A، مع فلترة صلة عقوبات Sina حتى لا تضخم إشارات قوائم حسابات الأوراق المالية عدّادات E2 ([#63](https://github.com/HKUDS/Vibe-Trading/pull/63)).
|
||||
- **2026-04-30** ⚙️ **إعدادات Web UI + تعزيز validation CLI**: تمت إضافة صفحة Settings لإعداد LLM provider/model وBase URL وreasoning effort وبيانات اعتماد مصادر البيانات محلياً. واجهات settings API محمية الآن عبر local/auth، كما أصبحت بيانات مزودي النماذج إعدادات مدفوعة بالبيانات ([#57](https://github.com/HKUDS/Vibe-Trading/pull/57)). كذلك تم تعزيز `python -m backtest.validation <run_dir>` لرفض غياب الوسيط، والمسار الفارغ، والمسار غير الصالح، والمسار غير الموجود، والمسار الذي ليس دليلاً برسائل واضحة قبل بدء التحقق ([#60](https://github.com/HKUDS/Vibe-Trading/pull/60)).
|
||||
- **2026-04-28** 🚀 **إصدار v0.1.6** (`pip install -U vibe-trading-ai`): إصلاح إرجاع `vibe-trading --swarm-presets` فارغًا بعد `pip install` / `uv tool install` ([#55](https://github.com/HKUDS/Vibe-Trading/issues/55)) — ملفات YAML للإعدادات المسبقة الآن مضمّنة داخل حزمة `src.swarm` ومثبّتة بـ 6 اختبارات انحدار. إضافة إلى ذلك، محمّل AKShare يوجّه الآن صناديق ETF (`510300.SH`) والعملات الأجنبية (`USDCNH`) إلى نقاط النهاية الصحيحة مع تعزيز سلسلة البديل. تجميع كل التحديثات منذ v0.1.5: لوحة مقارنة المرجع، تدفق `/upload` + حدود الحجم، محمّل Futu (HK + أسهم A)، مهارة تصدير vnpy، تصليب أمني، التحميل الكسول للواجهة (688KB → 262KB).
|
||||
|
||||
+2
-1
@@ -51,13 +51,14 @@
|
||||
|
||||
## 📰 ニュース
|
||||
|
||||
- **2026-05-05** 🛡️ **セキュリティ境界の追加強化**: 明示的な CORS origin、Settings の認証情報ステータス表示、Web URL 読み取り、Shadow Account コード生成まわりの残りのセキュリティ境界を補強し、それぞれに回帰テストを追加しました。localhost の CLI/Web UI ワークフローは従来どおりです。リモートデプロイでは引き続き `API_AUTH_KEY` と明示的な信頼済み origin を設定してください。
|
||||
- **2026-05-04** 🖥️ **インタラクティブCLI UX + CI整理**: インタラクティブモードに、provider/model、セッション時間、直近実行時間、累計ツール呼び出し統計を表示するライブ下部ステータスバーを追加。さらに `prompt_toolkit` により上下キーの履歴移動と左右キーのカーソル編集に対応しました([#69](https://github.com/HKUDS/Vibe-Trading/pull/69))。`prompt_toolkit` またはTTYが利用できない場合は、従来どおりRich promptにフォールバックします。CIのパス期待値も強化済みファイルimportサンドボックスとクロスプラットフォームな `/tmp` 解決に合わせ、mainはグリーンに戻りました([`bb67dc7`](https://github.com/HKUDS/Vibe-Trading/commit/bb67dc7cfcc11553c57d8962bee56381dca43758))。
|
||||
- **2026-05-03** 🛡️ **セキュリティハードニングパッチ**: 非ローカルデプロイ向けの既定API認証を強化し、機密性の高いrun/session/swarm読み取りを保護、アップロードとローカルファイル読み取り境界を制限、shell系ツールをエントリーポイント別に制御、生成戦略をimport前に検証し、Dockerイメージは既定で非rootユーザーかつlocalhost限定ポート公開で動作します。CLIとlocalhost Web UIは低摩擦のままです。リモートAPI/Webデプロイでは`API_AUTH_KEY`を設定してください。
|
||||
- **2026-05-02** 🧭 **配当分析 + ロードマップ刷新**: インカム株、配当の持続性、増配、株主還元利回り、権利落ちメカニクス、利回りの罠チェックに対応する `dividend-analysis` スキルを追加し、バンドルスキル回帰テストで固定しました。公開ロードマップは Research Autopilot、Data Bridge、Options Lab、Portfolio Studio、Alpha Zoo、Research Delivery、Trust Layer、Community 共有に絞りました。
|
||||
|
||||
<details>
|
||||
<summary>過去のニュース</summary>
|
||||
|
||||
- **2026-05-02** 🧭 **配当分析 + ロードマップ刷新**: インカム株、配当の持続性、増配、株主還元利回り、権利落ちメカニクス、利回りの罠チェックに対応する `dividend-analysis` スキルを追加し、バンドルスキル回帰テストで固定しました。公開ロードマップは Research Autopilot、Data Bridge、Options Lab、Portfolio Studio、Alpha Zoo、Research Delivery、Trust Layer、Community 共有に絞りました。
|
||||
- **2026-05-01** 🔥 **相関ヒートマップ + OpenAI Codex OAuth + A株 pre-ST フィルター**: 新しい相関ダッシュボード/APIでローリングリターン相関を計算し、ポートフォリオや銘柄分析向けに ECharts ヒートマップで可視化します([#64](https://github.com/HKUDS/Vibe-Trading/pull/64))。OpenAI Codex provider は `vibe-trading provider login openai-codex` による ChatGPT OAuth に対応し、Settings メタデータとアダプター回帰テストも追加([#65](https://github.com/HKUDS/Vibe-Trading/pull/65))。A株の ST/*ST リスクスクリーニング用 `ashare-pre-st-filter` スキルを追加・強化し、Sina 処分公告の関連性フィルターにより証券口座リスト内の言及が E2 回数を水増ししないようにしました([#63](https://github.com/HKUDS/Vibe-Trading/pull/63))。
|
||||
- **2026-04-30** ⚙️ **Web UI設定 + validation CLI強化**: LLM provider/model、Base URL、reasoning effort、データソース認証情報をローカルで設定できる Settings ページを追加。settings API は local/auth で保護され、provider メタデータもデータ駆動設定に移行([#57](https://github.com/HKUDS/Vibe-Trading/pull/57))。さらに `python -m backtest.validation <run_dir>` を強化し、引数なし・空パス・不正パス・存在しないパス・ディレクトリでないパスを検証開始前に分かりやすく失敗させます([#60](https://github.com/HKUDS/Vibe-Trading/pull/60))。
|
||||
- **2026-04-28** 🚀 **v0.1.6 リリース**(`pip install -U vibe-trading-ai`): `pip install` / `uv tool install` 後に `vibe-trading --swarm-presets` が空を返す問題を修正([#55](https://github.com/HKUDS/Vibe-Trading/issues/55))— プリセット YAML を `src.swarm` パッケージ内に同梱、6 件の回帰テストでピン留め。加えて AKShare ローダーが ETF(`510300.SH`)と外国為替(`USDCNH`)を正しいエンドポイントにルーティング、レジストリフォールバックも強化。v0.1.5 以降の更新を集約: ベンチマーク比較パネル、`/upload` ストリーミング + サイズ制限、Futu ローダー(HK + A 株)、vnpy エクスポートスキル、セキュリティ強化、フロントエンド遅延ロード(688KB → 262KB)。
|
||||
|
||||
+2
-1
@@ -51,13 +51,14 @@
|
||||
|
||||
## 📰 뉴스
|
||||
|
||||
- **2026-05-05** 🛡️ **보안 경계 후속 강화**: 명시적 CORS origin, Settings 자격 증명 상태 표시, 웹 URL 읽기, Shadow Account 코드 생성 주변의 남은 보안 경계를 보강하고 각 경로에 회귀 테스트를 추가했습니다. localhost CLI/Web UI 흐름은 그대로 유지됩니다. 원격 배포에서는 계속 `API_AUTH_KEY`와 명시적인 신뢰 origin을 사용하세요.
|
||||
- **2026-05-04** 🖥️ **대화형 CLI UX + CI 정리**: 대화형 모드에 provider/model, 세션 시간, 직전 실행 시간, 누적 도구 호출 통계를 보여주는 실시간 하단 상태 표시줄이 추가되었습니다. 또한 `prompt_toolkit`을 통해 위/아래 방향키 히스토리 탐색과 좌/우 방향키 커서 편집을 지원합니다([#69](https://github.com/HKUDS/Vibe-Trading/pull/69)). `prompt_toolkit` 또는 TTY를 사용할 수 없으면 기존 Rich prompt로 자동 폴백합니다. CI 경로 기대값도 강화된 파일 import 샌드박스와 크로스플랫폼 `/tmp` 해석에 맞춰 정리되어 main이 다시 green 상태가 되었습니다([`bb67dc7`](https://github.com/HKUDS/Vibe-Trading/commit/bb67dc7cfcc11553c57d8962bee56381dca43758)).
|
||||
- **2026-05-03** 🛡️ **보안 강화 패치**: 비로컬 배포의 기본 API 인증을 강화하고, 민감한 run/session/swarm 읽기 API를 보호하며, 업로드와 로컬 파일 읽기 경계를 제한하고, shell 가능 도구를 진입점별로 제어합니다. 생성된 전략은 import 전에 검증되며 Docker 이미지는 기본적으로 비root 사용자와 localhost 전용 포트 공개로 실행됩니다. CLI와 localhost Web UI 흐름은 낮은 마찰을 유지합니다. 원격 API/Web 배포에서는 `API_AUTH_KEY`를 설정하세요.
|
||||
- **2026-05-02** 🧭 **배당 분석 + 더 선명한 로드맵**: 인컴 주식, 배당 지속 가능성, 배당 성장, 주주환원 수익률, 배당락 메커니즘, 고배당 함정 점검을 다루는 `dividend-analysis` 스킬을 추가하고 bundled skill 회귀 테스트로 고정했습니다. 공개 로드맵은 Research Autopilot, Data Bridge, Options Lab, Portfolio Studio, Alpha Zoo, Research Delivery, Trust Layer, Community 공유에 집중하도록 정리했습니다.
|
||||
|
||||
<details>
|
||||
<summary>이전 뉴스</summary>
|
||||
|
||||
- **2026-05-02** 🧭 **배당 분석 + 더 선명한 로드맵**: 인컴 주식, 배당 지속 가능성, 배당 성장, 주주환원 수익률, 배당락 메커니즘, 고배당 함정 점검을 다루는 `dividend-analysis` 스킬을 추가하고 bundled skill 회귀 테스트로 고정했습니다. 공개 로드맵은 Research Autopilot, Data Bridge, Options Lab, Portfolio Studio, Alpha Zoo, Research Delivery, Trust Layer, Community 공유에 집중하도록 정리했습니다.
|
||||
- **2026-05-01** 🔥 **상관관계 히트맵 + OpenAI Codex OAuth + A주 pre-ST 필터**: 새 상관관계 대시보드/API가 롤링 수익률 상관관계를 계산하고, 포트폴리오 및 종목 분석용 ECharts 히트맵으로 렌더링합니다([#64](https://github.com/HKUDS/Vibe-Trading/pull/64)). OpenAI Codex provider는 이제 `vibe-trading provider login openai-codex`로 ChatGPT OAuth를 사용할 수 있으며, Settings 메타데이터와 어댑터 회귀 테스트도 추가되었습니다([#65](https://github.com/HKUDS/Vibe-Trading/pull/65)). A주 ST/*ST 리스크 스크리닝을 위한 `ashare-pre-st-filter` 스킬을 추가하고 강화했으며, Sina 제재 공시 관련성 필터링으로 증권 계좌 목록 언급이 E2 횟수를 부풀리지 않도록 했습니다([#63](https://github.com/HKUDS/Vibe-Trading/pull/63)).
|
||||
- **2026-04-30** ⚙️ **Web UI 설정 + validation CLI 강화**: LLM provider/model, Base URL, reasoning effort, 데이터 소스 자격 증명을 로컬에서 설정할 수 있는 Settings 페이지를 추가했습니다. settings API는 local/auth로 보호되며 provider 메타데이터도 데이터 기반 설정으로 분리되었습니다([#57](https://github.com/HKUDS/Vibe-Trading/pull/57)). 또한 `python -m backtest.validation <run_dir>`가 인자 없음, 빈 경로, 잘못된 경로, 존재하지 않는 경로, 디렉터리가 아닌 경로를 검증 시작 전에 명확한 메시지로 실패하도록 강화했습니다([#60](https://github.com/HKUDS/Vibe-Trading/pull/60)).
|
||||
- **2026-04-28** 🚀 **v0.1.6 릴리스**(`pip install -U vibe-trading-ai`): `pip install` / `uv tool install` 설치 후 `vibe-trading --swarm-presets`가 비어 있는 문제 수정([#55](https://github.com/HKUDS/Vibe-Trading/issues/55)) — 프리셋 YAML을 `src.swarm` 패키지 내부에 번들링, 6개 회귀 테스트로 고정. 또한 AKShare 로더가 ETF(`510300.SH`)와 외환(`USDCNH`)을 올바른 엔드포인트로 라우팅하고 레지스트리 폴백 강화. v0.1.5 이후 업데이트 종합: 벤치마크 비교 패널, `/upload` 스트리밍 + 크기 제한, Futu 로더(HK + A주), vnpy 내보내기 스킬, 보안 강화, 프론트엔드 지연 로딩(688KB → 262KB).
|
||||
|
||||
+2
-1
@@ -51,13 +51,14 @@
|
||||
|
||||
## 📰 新闻
|
||||
|
||||
- **2026-05-05** 🛡️ **安全边界补充加固**:补齐显式 CORS origin、Settings 凭据状态提示、网页 URL 读取与 Shadow Account 代码生成相关的安全边界,并为这些路径加入回归测试。本机 CLI/Web UI 使用方式不变;远程部署仍应使用 `API_AUTH_KEY` 和显式可信 origin。
|
||||
- **2026-05-04** 🖥️ **交互式 CLI 体验 + CI 清理**:交互模式新增实时底部状态栏,可显示 provider/model、会话时长、上次运行耗时和累计工具调用统计;同时通过 `prompt_toolkit` 支持方向键历史浏览与光标编辑([#69](https://github.com/HKUDS/Vibe-Trading/pull/69))。当 `prompt_toolkit` 或 TTY 不可用时,CLI 仍会回退到 Rich prompt。CI 路径断言也已对齐新的文件导入沙箱和跨平台 `/tmp` 解析,main 已恢复绿色([`bb67dc7`](https://github.com/HKUDS/Vibe-Trading/commit/bb67dc7cfcc11553c57d8962bee56381dca43758))。
|
||||
- **2026-05-03** 🛡️ **安全加固补丁**:收紧非本地部署的默认 API 鉴权,保护敏感的 run/session/swarm 读取接口,限制上传与本地文件读取边界,按入口类型控制 shell 能力工具,在导入前校验生成策略,并让 Docker 镜像默认以非 root 用户运行且只发布到 localhost。CLI 与本机 Web UI 仍保持低摩擦;远程 API/Web 部署应配置 `API_AUTH_KEY`。
|
||||
- **2026-05-02** 🧭 **分红分析 + 更清晰的路线图**:新增 `dividend-analysis` 技能,覆盖收益型股票、分红可持续性、股息增长、股东回报率、除息机制与高息陷阱检查,并用 bundled skill 回归测试固定。公开路线图现在聚焦未来工作:Research Autopilot、Data Bridge、Options Lab、Portfolio Studio、Alpha Zoo、Research Delivery、Trust Layer 和 Community 分享。
|
||||
|
||||
<details>
|
||||
<summary>更早的新闻</summary>
|
||||
|
||||
- **2026-05-02** 🧭 **分红分析 + 更清晰的路线图**:新增 `dividend-analysis` 技能,覆盖收益型股票、分红可持续性、股息增长、股东回报率、除息机制与高息陷阱检查,并用 bundled skill 回归测试固定。公开路线图现在聚焦未来工作:Research Autopilot、Data Bridge、Options Lab、Portfolio Studio、Alpha Zoo、Research Delivery、Trust Layer 和 Community 分享。
|
||||
- **2026-05-01** 🔥 **相关性热力图 + OpenAI Codex OAuth + A 股 pre-ST 过滤器**:新增相关性仪表盘/API,可计算滚动收益相关性,并用 ECharts 热力图展示组合与标的相关结构([#64](https://github.com/HKUDS/Vibe-Trading/pull/64))。OpenAI Codex provider 现支持通过 `vibe-trading provider login openai-codex` 使用 ChatGPT OAuth,并补齐 Settings 元数据与适配器回归测试([#65](https://github.com/HKUDS/Vibe-Trading/pull/65))。新增并加固 `ashare-pre-st-filter` 技能,用于 A 股 ST/*ST 风险筛查;Sina 处罚公告相关性过滤会避免证券账户名单提及误计入 E2 频次([#63](https://github.com/HKUDS/Vibe-Trading/pull/63))。
|
||||
- **2026-04-30** ⚙️ **Web UI 设置页 + validation CLI 加固**:新增 Settings 页面,可在本地配置 LLM provider/model、Base URL、reasoning effort 以及数据源凭据;对应 settings API 已加本地/鉴权保护,并把 provider 元数据改为数据驱动配置([#57](https://github.com/HKUDS/Vibe-Trading/pull/57))。同时加固 `python -m backtest.validation <run_dir>`:缺参、空路径、非法路径、不存在路径、非目录路径都会在验证开始前给出明确错误([#60](https://github.com/HKUDS/Vibe-Trading/pull/60))。
|
||||
- **2026-04-28** 🚀 **v0.1.6 发布**(`pip install -U vibe-trading-ai`):修复 `pip install` / `uv tool install` 安装后 `vibe-trading --swarm-presets` 返回空的问题([#55](https://github.com/HKUDS/Vibe-Trading/issues/55))—— 预设 YAML 现已打包进 `src.swarm` 包内,配套 6 个回归测试。同时 AKShare 加载器正确路由 ETF(`510300.SH`)和外汇(`USDCNH`)到对应端点,并加固注册表回退链。汇总自 v0.1.5 以来全部更新:基准对比面板、`/upload` 流式上传 + 大小限制、富途数据源(港股/A 股)、vnpy 导出技能、安全加固、前端懒加载(688KB → 262KB)。
|
||||
|
||||
+40
-19
@@ -249,12 +249,43 @@ app = FastAPI(
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# CORS: override with CORS_ORIGINS (comma-separated)
|
||||
_CORS_ORIGINS = os.getenv(
|
||||
"CORS_ORIGINS",
|
||||
"http://localhost:3000,http://localhost:5173,http://localhost:8000,"
|
||||
"http://127.0.0.1:3000,http://127.0.0.1:5173,http://127.0.0.1:8000",
|
||||
).split(",")
|
||||
_DEFAULT_CORS_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:8000",
|
||||
]
|
||||
|
||||
|
||||
def _parse_cors_origins(raw: Optional[str]) -> List[str]:
|
||||
"""Parse CORS origins and reject credentialed wildcard configuration.
|
||||
|
||||
Args:
|
||||
raw: Comma-separated CORS origins from ``CORS_ORIGINS``. ``None`` or a
|
||||
blank value uses the loopback development defaults.
|
||||
|
||||
Returns:
|
||||
Explicit CORS origins accepted by the API server.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If a wildcard origin is configured while credentials are
|
||||
enabled.
|
||||
"""
|
||||
if raw is None or not raw.strip():
|
||||
return list(_DEFAULT_CORS_ORIGINS)
|
||||
origins = [origin.strip() for origin in raw.split(",") if origin.strip()]
|
||||
if "*" in origins:
|
||||
raise RuntimeError(
|
||||
"CORS_ORIGINS='*' is not allowed while credentials are enabled; "
|
||||
"configure explicit Web UI origins instead."
|
||||
)
|
||||
return origins
|
||||
|
||||
|
||||
# CORS: override with CORS_ORIGINS (comma-separated explicit origins)
|
||||
_CORS_ORIGINS = _parse_cors_origins(os.getenv("CORS_ORIGINS"))
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -571,16 +602,6 @@ def _write_env_values(path: Path, updates: Dict[str, str]) -> None:
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _mask_secret(value: str) -> Optional[str]:
|
||||
"""Return a non-sensitive hint for a configured secret."""
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
if len(value) <= 8:
|
||||
return "****"
|
||||
return f"{value[:4]}...{value[-4:]}"
|
||||
|
||||
|
||||
def _is_configured_secret(value: str, placeholders: set[str]) -> bool:
|
||||
"""Return True when a secret is set and not a documented placeholder."""
|
||||
normalized = value.strip().strip('"').strip("'")
|
||||
@@ -610,7 +631,7 @@ def _build_llm_settings_response(values: Optional[Dict[str, str]] = None) -> LLM
|
||||
provider = LLM_PROVIDER_BY_NAME.get(provider_name, LLM_PROVIDER_BY_NAME["openai"])
|
||||
api_key = env_values.get(provider.api_key_env or "", "") if provider.api_key_env else ""
|
||||
api_key_configured = _is_configured_secret(api_key, LLM_API_KEY_PLACEHOLDERS)
|
||||
api_key_hint = _mask_secret(api_key) if api_key_configured else None
|
||||
api_key_hint = None
|
||||
if provider.auth_type == "oauth":
|
||||
try:
|
||||
from src.providers.openai_codex import get_openai_codex_login_status
|
||||
@@ -619,7 +640,7 @@ def _build_llm_settings_response(values: Optional[Dict[str, str]] = None) -> LLM
|
||||
except Exception:
|
||||
token = None
|
||||
api_key_configured = bool(token)
|
||||
api_key_hint = getattr(token, "account_id", None) if token else None
|
||||
api_key_hint = None
|
||||
return LLMSettingsResponse(
|
||||
provider=provider.name,
|
||||
model_name=env_values.get("LANGCHAIN_MODEL_NAME", provider.default_model),
|
||||
@@ -665,7 +686,7 @@ def _build_data_source_settings_response(values: Optional[Dict[str, str]] = None
|
||||
baostock_message = "No BaoStock loader is registered in this project."
|
||||
return DataSourceSettingsResponse(
|
||||
tushare_token_configured=token_configured,
|
||||
tushare_token_hint=_mask_secret(token) if token_configured else None,
|
||||
tushare_token_hint=None,
|
||||
baostock_supported=supported,
|
||||
baostock_installed=installed,
|
||||
baostock_message=baostock_message,
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -24,14 +25,48 @@ _TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
_SIGNAL_ENGINE_TEMPLATE = "signal_engine.py.j2"
|
||||
|
||||
|
||||
def _literal_safe_value(value: Any) -> Any:
|
||||
"""Return a value that can be faithfully represented as a Python literal."""
|
||||
if value is None or isinstance(value, (str, bool, int)):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
if not math.isfinite(value):
|
||||
raise ValueError("Non-finite floats cannot be rendered as Python literals")
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return [_literal_safe_value(item) for item in value]
|
||||
if isinstance(value, tuple):
|
||||
return tuple(_literal_safe_value(item) for item in value)
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
_literal_safe_value(key): _literal_safe_value(item)
|
||||
for key, item in value.items()
|
||||
}
|
||||
raise TypeError(
|
||||
f"Unsupported generated Python literal type: {type(value).__name__}",
|
||||
)
|
||||
|
||||
|
||||
def _python_literal(value: Any) -> str:
|
||||
"""Render ``value`` as source text for a safe Python literal."""
|
||||
literal = repr(_literal_safe_value(value))
|
||||
try:
|
||||
ast.literal_eval(literal)
|
||||
except (SyntaxError, ValueError) as exc:
|
||||
raise ValueError(f"Invalid generated Python literal: {literal!r}") from exc
|
||||
return literal
|
||||
|
||||
|
||||
def _env() -> Environment:
|
||||
"""Jinja2 environment rooted at our templates directory."""
|
||||
return Environment(
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(str(_TEMPLATES_DIR)),
|
||||
autoescape=select_autoescape(enabled_extensions=("html", "xml")),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
env.filters["py_literal"] = _python_literal
|
||||
return env
|
||||
|
||||
|
||||
def _rule_to_context(rule: ShadowRule) -> dict[str, Any]:
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
"""Shadow Account generated signal engine — {{ shadow_id }}
|
||||
|
||||
Rules distilled from {{ profitable_roundtrips }} profitable roundtrips
|
||||
over {{ date_range[0] }} → {{ date_range[1] }}.
|
||||
"""Shadow Account generated signal engine.
|
||||
|
||||
This file is auto-generated; do not hand-edit. Regenerate via
|
||||
`src.shadow_account.codegen.render_signal_engine`.
|
||||
``src.shadow_account.codegen.render_signal_engine``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
SHADOW_ID = "{{ shadow_id }}"
|
||||
PREFERRED_MARKETS = {{ preferred_markets | tojson }}
|
||||
SHADOW_ID = {{ shadow_id | py_literal }}
|
||||
PROFITABLE_ROUNDTRIPS = {{ profitable_roundtrips | py_literal }}
|
||||
DATE_RANGE = {{ date_range | py_literal }}
|
||||
PREFERRED_MARKETS = {{ preferred_markets | py_literal }}
|
||||
|
||||
RULES = [
|
||||
{% for rule in rules %} {
|
||||
"rule_id": "{{ rule.rule_id }}",
|
||||
"market": {{ rule.market | tojson }},
|
||||
"entry_hour_min": {{ rule.entry_hour_min }},
|
||||
"entry_hour_max": {{ rule.entry_hour_max }},
|
||||
"hold_days": {{ rule.hold_days }},
|
||||
"weight": {{ rule.weight }},
|
||||
},
|
||||
{% endfor %}]
|
||||
RULES = {{ rules | py_literal }}
|
||||
|
||||
|
||||
def _market_of(code: str) -> str:
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import requests
|
||||
|
||||
@@ -13,6 +15,43 @@ _TIMEOUT = 30
|
||||
_MAX_LENGTH = 8000
|
||||
|
||||
|
||||
def _url_allowed(url: str) -> tuple[bool, str]:
|
||||
"""Return whether a URL is safe to forward to the remote reader service."""
|
||||
try:
|
||||
parsed = urlsplit(url.strip())
|
||||
except ValueError:
|
||||
return False, "target URL is not allowed"
|
||||
|
||||
if parsed.scheme.lower() not in {"http", "https"}:
|
||||
return False, "target URL is not allowed"
|
||||
if not parsed.hostname:
|
||||
return False, "target URL is not allowed"
|
||||
if parsed.username or parsed.password:
|
||||
return False, "target URL is not allowed"
|
||||
|
||||
host = parsed.hostname.rstrip(".").lower()
|
||||
if host == "localhost" or host.endswith(".localhost") or host.endswith(".local"):
|
||||
return False, "target URL is not allowed"
|
||||
|
||||
ip_host = host.split("%", 1)[0]
|
||||
try:
|
||||
ip = ipaddress.ip_address(ip_host)
|
||||
except ValueError:
|
||||
return True, ""
|
||||
|
||||
if (
|
||||
ip.is_private
|
||||
or ip.is_loopback
|
||||
or ip.is_link_local
|
||||
or ip.is_multicast
|
||||
or ip.is_reserved
|
||||
or ip.is_unspecified
|
||||
or not ip.is_global
|
||||
):
|
||||
return False, "target URL is not allowed"
|
||||
return True, ""
|
||||
|
||||
|
||||
def read_url(url: str) -> str:
|
||||
"""Fetch web page content via the Jina Reader API.
|
||||
|
||||
@@ -22,9 +61,14 @@ def read_url(url: str) -> str:
|
||||
Returns:
|
||||
JSON-formatted result containing title, content, and url.
|
||||
"""
|
||||
target_url = url.strip()
|
||||
allowed, error = _url_allowed(target_url)
|
||||
if not allowed:
|
||||
return json.dumps({"status": "error", "error": error}, ensure_ascii=False)
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{_JINA_PREFIX}{url}",
|
||||
f"{_JINA_PREFIX}{target_url}",
|
||||
headers={"Accept": "text/markdown"},
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
@@ -47,7 +91,7 @@ def read_url(url: str) -> str:
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
"title": title,
|
||||
"url": url,
|
||||
"url": target_url,
|
||||
"content": text,
|
||||
"length": len(resp.text),
|
||||
}, ensure_ascii=False)
|
||||
|
||||
@@ -145,3 +145,25 @@ def test_shell_tools_remote_api_request_accepts_explicit_opt_in(
|
||||
monkeypatch.setenv("VIBE_TRADING_ENABLE_SHELL_TOOLS", "1")
|
||||
|
||||
assert api_server._shell_tools_enabled_for_request(request)
|
||||
|
||||
|
||||
def test_default_cors_origins_are_loopback_only() -> None:
|
||||
origins = api_server._parse_cors_origins(None)
|
||||
|
||||
assert origins
|
||||
assert "*" not in origins
|
||||
assert all(
|
||||
origin.startswith("http://localhost:") or origin.startswith("http://127.0.0.1:")
|
||||
for origin in origins
|
||||
)
|
||||
|
||||
|
||||
def test_cors_origins_reject_credentialed_wildcard() -> None:
|
||||
with pytest.raises(RuntimeError, match="CORS_ORIGINS"):
|
||||
api_server._parse_cors_origins("https://app.example.com,*")
|
||||
|
||||
|
||||
def test_cors_origins_accept_explicit_remote_origins() -> None:
|
||||
origins = api_server._parse_cors_origins(" https://app.example.com,https://admin.example.com ")
|
||||
|
||||
assert origins == ["https://app.example.com", "https://admin.example.com"]
|
||||
@@ -103,7 +103,9 @@ def test_update_llm_settings_persists_project_env(
|
||||
body = response.json()
|
||||
assert body["provider"] == "openrouter"
|
||||
assert body["api_key_configured"] is True
|
||||
assert body["api_key_hint"] == "or-s...alue"
|
||||
assert body["api_key_hint"] is None
|
||||
assert "or-secret-value" not in response.text
|
||||
assert "or-s...alue" not in response.text
|
||||
|
||||
env_text = (tmp_path / ".env").read_text(encoding="utf-8")
|
||||
assert "LANGCHAIN_PROVIDER=openrouter" in env_text
|
||||
@@ -128,6 +130,38 @@ def test_get_data_source_settings_treats_placeholder_as_unconfigured(
|
||||
assert not (tmp_path / ".env").exists()
|
||||
|
||||
|
||||
def test_settings_response_never_exposes_configured_secret_hints(
|
||||
client: TestClient, tmp_path: Path,
|
||||
) -> None:
|
||||
(tmp_path / ".env").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"LANGCHAIN_PROVIDER=openrouter",
|
||||
"OPENROUTER_API_KEY=or-secret-private-value",
|
||||
"TUSHARE_TOKEN=ts-secret-private-token",
|
||||
]
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
llm_response = client.get("/settings/llm")
|
||||
data_response = client.get("/settings/data-sources")
|
||||
|
||||
assert llm_response.status_code == 200
|
||||
assert data_response.status_code == 200
|
||||
llm_body = llm_response.json()
|
||||
data_body = data_response.json()
|
||||
assert llm_body["api_key_configured"] is True
|
||||
assert llm_body["api_key_hint"] is None
|
||||
assert data_body["tushare_token_configured"] is True
|
||||
assert data_body["tushare_token_hint"] is None
|
||||
assert "or-secret-private-value" not in llm_response.text
|
||||
assert "or-s...alue" not in llm_response.text
|
||||
assert "ts-secret-private-token" not in data_response.text
|
||||
assert "ts-s...oken" not in data_response.text
|
||||
|
||||
|
||||
def test_settings_reads_reject_remote_dev_mode_clients(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -188,7 +222,10 @@ def test_settings_reads_require_bearer_when_api_auth_key_is_configured(
|
||||
|
||||
assert unauthenticated_response.status_code == 401
|
||||
assert authenticated_response.status_code == 200
|
||||
assert authenticated_response.json()["api_key_hint"] == "or-s...alue"
|
||||
assert authenticated_response.json()["api_key_configured"] is True
|
||||
assert authenticated_response.json()["api_key_hint"] is None
|
||||
assert "or-secret-value" not in authenticated_response.text
|
||||
assert "or-s...alue" not in authenticated_response.text
|
||||
|
||||
|
||||
def test_update_data_source_settings_persists_tushare_token(
|
||||
@@ -202,7 +239,9 @@ def test_update_data_source_settings_persists_tushare_token(
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["tushare_token_configured"] is True
|
||||
assert body["tushare_token_hint"] == "ts-s...oken"
|
||||
assert body["tushare_token_hint"] is None
|
||||
assert "ts-secret-token" not in response.text
|
||||
assert "ts-s...oken" not in response.text
|
||||
|
||||
env_text = (tmp_path / ".env").read_text(encoding="utf-8")
|
||||
assert "TUSHARE_TOKEN=ts-secret-token" in env_text
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Security tests for Shadow Account code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
|
||||
import pytest
|
||||
|
||||
from src.shadow_account.codegen import render_signal_engine, validate_generated
|
||||
from src.shadow_account.models import ShadowProfile, ShadowRule
|
||||
|
||||
|
||||
def _malicious_profile() -> ShadowProfile:
|
||||
"""Build a profile whose dynamic strings look like Python code."""
|
||||
rule_id = 'R1"""\nINJECTED_RULE_ID = "boom"\n"""'
|
||||
market = 'china_a"""\nINJECTED_MARKET = "boom"\n"""'
|
||||
return ShadowProfile(
|
||||
shadow_id='shadow_abc"""\nINJECTED_SHADOW_ID = "boom"\n"""',
|
||||
created_at="2026-05-05T00:00:00Z",
|
||||
journal_hash="deadbeef",
|
||||
source_market="china_a",
|
||||
profitable_roundtrips=7,
|
||||
total_roundtrips=11,
|
||||
date_range=(
|
||||
'2026-01-01"; INJECTED_START = "boom"; #',
|
||||
'2026-01-31\nINJECTED_END = "boom"',
|
||||
),
|
||||
profile_text="security regression profile",
|
||||
rules=(
|
||||
ShadowRule(
|
||||
rule_id=rule_id,
|
||||
human_text="security regression rule",
|
||||
entry_condition={
|
||||
"market": market,
|
||||
"entry_hour": {"min": 9, "max": 15},
|
||||
},
|
||||
exit_condition={},
|
||||
holding_days_range=(2, 4),
|
||||
support_count=3,
|
||||
coverage_rate=0.42,
|
||||
sample_trades=("600519.SH@2026-01-05",),
|
||||
weight=0.75,
|
||||
),
|
||||
),
|
||||
preferred_markets=(
|
||||
'china_a"; INJECTED_PREFERRED = "boom"; #',
|
||||
'us\nINJECTED_PREFERRED_2 = "boom"',
|
||||
),
|
||||
typical_holding_days=(2.0, 4.0),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_render_signal_engine_escapes_python_literals_for_dynamic_values() -> None:
|
||||
"""LLM/user strings must stay data, never executable generated code."""
|
||||
profile = _malicious_profile()
|
||||
source = render_signal_engine(profile)
|
||||
|
||||
tree = ast.parse(source)
|
||||
ok, err = validate_generated(source)
|
||||
assert ok, f"generated source failed validation: {err}"
|
||||
|
||||
stored_names = {
|
||||
node.id
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)
|
||||
}
|
||||
assert not any(name.startswith("INJECTED_") for name in stored_names)
|
||||
|
||||
namespace: dict[str, object] = {"__name__": "shadow_codegen_security_test"}
|
||||
exec(compile(source, "<generated-signal-engine>", "exec"), namespace)
|
||||
|
||||
assert not any(name.startswith("INJECTED_") for name in namespace)
|
||||
assert namespace["SHADOW_ID"] == profile.shadow_id
|
||||
assert namespace["PREFERRED_MARKETS"] == list(profile.preferred_markets)
|
||||
|
||||
rules = namespace["RULES"]
|
||||
assert isinstance(rules, list)
|
||||
assert rules[0]["rule_id"] == profile.rules[0].rule_id
|
||||
assert rules[0]["market"] == profile.rules[0].entry_condition["market"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Security tests for the web reader tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.tools import web_reader_tool
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"",
|
||||
"file:///etc/passwd",
|
||||
"ftp://example.com/report",
|
||||
"https:///missing-host",
|
||||
"https://user:pass@example.com/private",
|
||||
"http://localhost:8899/health",
|
||||
"http://api.localhost/health",
|
||||
"http://service.local/status",
|
||||
"http://127.0.0.1:8899/health",
|
||||
"http://0.0.0.0:8899/health",
|
||||
"http://10.0.0.5/metadata",
|
||||
"http://172.16.0.5/metadata",
|
||||
"http://192.168.1.5/metadata",
|
||||
"http://169.254.169.254/latest/meta-data",
|
||||
"http://224.0.0.1/",
|
||||
"http://[::1]/",
|
||||
"http://[fc00::1]/",
|
||||
"http://[fe80::1]/",
|
||||
],
|
||||
)
|
||||
def test_read_url_rejects_non_public_targets_without_network(
|
||||
monkeypatch: pytest.MonkeyPatch, url: str,
|
||||
) -> None:
|
||||
def fail_get(*_args: object, **_kwargs: object) -> None:
|
||||
raise AssertionError("requests.get should not be called for blocked URLs")
|
||||
|
||||
monkeypatch.setattr(web_reader_tool.requests, "get", fail_get)
|
||||
|
||||
result = json.loads(web_reader_tool.read_url(url))
|
||||
|
||||
assert result["status"] == "error"
|
||||
assert "target URL is not allowed" in result["error"]
|
||||
|
||||
|
||||
def test_read_url_allows_public_http_targets(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: object) -> SimpleNamespace:
|
||||
calls.append({"url": url, **kwargs})
|
||||
return SimpleNamespace(
|
||||
status_code=200,
|
||||
text="Title: Example\n\n# Example\n\nPublic content",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(web_reader_tool.requests, "get", fake_get)
|
||||
|
||||
result = json.loads(web_reader_tool.read_url("https://example.com/docs?x=1"))
|
||||
|
||||
assert result["status"] == "ok"
|
||||
assert result["title"] == "Example"
|
||||
assert result["url"] == "https://example.com/docs?x=1"
|
||||
assert calls == [
|
||||
{
|
||||
"url": "https://r.jina.ai/https://example.com/docs?x=1",
|
||||
"headers": {"Accept": "text/markdown"},
|
||||
"timeout": 30,
|
||||
}
|
||||
]
|
||||
@@ -18,7 +18,7 @@ const messages = {
|
||||
llmModelName: "Model",
|
||||
llmBaseUrl: "Base URL",
|
||||
llmApiKey: "API key",
|
||||
llmApiKeyConfigured: "Configured as {key}",
|
||||
llmApiKeyConfigured: "Configured",
|
||||
llmApiKeyPlaceholder: "Leave blank to keep the current key",
|
||||
llmClearApiKey: "Clear saved API key",
|
||||
llmNoApiKeyRequired: "This provider does not require an API key.",
|
||||
@@ -40,7 +40,7 @@ const messages = {
|
||||
dataSourceSettings: "Data Source Settings",
|
||||
dataSourceSettingsDesc: "Configure optional market data credentials used by backtests and research agents.",
|
||||
tushareToken: "Tushare token",
|
||||
tushareTokenConfigured: "Configured as {key}",
|
||||
tushareTokenConfigured: "Configured",
|
||||
tushareTokenPlaceholder: "Leave blank to keep the current token",
|
||||
tushareTokenHint: "Used for China A-share, futures, fund, and macro data. If unset, the project falls back to AKShare where available.",
|
||||
clearTushareToken: "Clear saved Tushare token",
|
||||
|
||||
@@ -207,16 +207,16 @@ export function Settings() {
|
||||
);
|
||||
}
|
||||
|
||||
const keyStatus = settings.api_key_hint
|
||||
? t.llmApiKeyConfigured.replace("{key}", settings.api_key_hint)
|
||||
const keyStatus = settings.api_key_configured
|
||||
? t.llmApiKeyConfigured
|
||||
: settings.api_key_required
|
||||
? t.llmApiKeyPlaceholder
|
||||
: selectedProvider?.auth_type === "oauth" && selectedProvider.login_command
|
||||
? t.llmOauthRequired.replace("{command}", selectedProvider.login_command)
|
||||
: t.llmNoApiKeyRequired;
|
||||
const apiKeyDisabled = !selectedProvider?.api_key_required || clearApiKey;
|
||||
const tushareStatus = dataSettings.tushare_token_hint
|
||||
? t.tushareTokenConfigured.replace("{key}", dataSettings.tushare_token_hint)
|
||||
const tushareStatus = dataSettings.tushare_token_configured
|
||||
? t.tushareTokenConfigured
|
||||
: t.tushareTokenPlaceholder;
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user