diff --git a/SECURITY.md b/SECURITY.md index 16f4882..8d3b187 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,14 @@ If you suspect tokens, passwords, API keys, service-account keys, cookies, broke 2. Pause scheduled jobs, deployments, or external integrations if the exposure can affect automation, artifact publishing, notifications, or trading behavior. 3. Remove the exposed material from open pull requests, issues, logs, and artifacts. 4. Coordinate any required history rewrite or downstream credential update with the maintainer. +5. Do not use an exposed value to test, identify, or revoke a credential. Rotate from the owning provider or control plane, then update the approved secret store and verify the runtime path. +6. Close a secret-scanning alert as `revoked` only after rotation and runtime verification. A history rewrite is a follow-up control, never a substitute for rotation. + +## Public Configuration and Runtime Routing + +- Public configuration may describe a runtime variable or secret reference, but must not contain a production notification target, account identifier, token, or credential value. +- `notifications.quant_sentinel.telegram_chat_id_ref` is the canonical cross-platform contract: runtime prefers `QSL_GLOBAL_TELEGRAM_CHAT_ID` and can fall back to the documented compatibility variables. +- Runtime resource names and workflow topology are not credentials. Keep them accurate for reproducible operations; migrate only values that are not required by public build or deployment contracts. ## Scope Notes diff --git a/docs/notifications-quant-sentinel.zh-CN.md b/docs/notifications-quant-sentinel.zh-CN.md index c58c414..5971867 100644 --- a/docs/notifications-quant-sentinel.zh-CN.md +++ b/docs/notifications-quant-sentinel.zh-CN.md @@ -16,9 +16,10 @@ | 变量 | 说明 | |------|------| | `TELEGRAM_TOKEN` | bot token(Cloud Run 由 secret ref 注入;VPS 由 `load_telegram_env.sh`) | -| `GLOBAL_TELEGRAM_CHAT_ID` | `` | +| `QSL_GLOBAL_TELEGRAM_CHAT_ID` | 首选跨平台路由变量(运行环境注入) | +| `GLOBAL_TELEGRAM_CHAT_ID` | 兼容回退变量(运行环境注入) | -别名见 `platform-config.json` → `notifications.quant_sentinel.env_aliases`。 +公开 `platform-config.json` 只记录 `notifications.quant_sentinel.telegram_chat_id_ref`,不保存实际通知目标;别名见 `env_aliases`。实际值只能从 GitHub/Cloud 受控运行环境注入。 ## VPS diff --git a/platform-config.json b/platform-config.json index bc861e0..241697b 100644 --- a/platform-config.json +++ b/platform-config.json @@ -20,7 +20,11 @@ "label_zh": "量化哨兵", "label_en": "QuantSentinel", "telegram_bot_token_secret_name": "quant-sentinel-telegram-bot-token", - "telegram_chat_id": "5992562050", + "telegram_chat_id_ref": { + "source": "runtime_environment", + "preferred_env": "QSL_GLOBAL_TELEGRAM_CHAT_ID", + "fallback_envs": ["GLOBAL_TELEGRAM_CHAT_ID", "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS"] + }, "gcp_projects": [ "firstradequant", "longbridgequant", @@ -29,7 +33,7 @@ ], "env_aliases": { "bot_token": ["TELEGRAM_TOKEN", "TG_TOKEN", "STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN"], - "chat_id": ["GLOBAL_TELEGRAM_CHAT_ID", "QSL_GLOBAL_TELEGRAM_CHAT_ID", "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS"] + "chat_id": ["QSL_GLOBAL_TELEGRAM_CHAT_ID", "GLOBAL_TELEGRAM_CHAT_ID", "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS"] }, "deprecated_secret_names": ["crisis-alert-telegram-bot-token"] } diff --git a/python/scripts/build_config.py b/python/scripts/build_config.py index 32c1e9f..baf4feb 100644 --- a/python/scripts/build_config.py +++ b/python/scripts/build_config.py @@ -67,6 +67,13 @@ "p4_p6_definition": "UNDEFINED", } +QUANT_SENTINEL_CHAT_ID_SOURCE = "runtime_environment" +QUANT_SENTINEL_CHAT_ID_PREFERRED_ENV = "QSL_GLOBAL_TELEGRAM_CHAT_ID" +QUANT_SENTINEL_CHAT_ID_FALLBACK_ENVS = ( + "GLOBAL_TELEGRAM_CHAT_ID", + "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS", +) + def load_config() -> dict: with open(CONFIG_PATH) as f: @@ -76,6 +83,7 @@ def load_config() -> dict: def validate(config: dict) -> list[str]: errors: list[str] = [] validate_runtime_authority_status(config, errors) + validate_quant_sentinel_notification_reference(config, errors) scheduling = config.get("scheduling") scheduler_profiles = scheduling.get("profiles") if isinstance(scheduling, dict) else None if not isinstance(scheduler_profiles, dict) or not scheduler_profiles: @@ -302,6 +310,53 @@ def validate(config: dict) -> list[str]: return errors +def validate_quant_sentinel_notification_reference(config: dict, errors: list[str]) -> None: + """Keep notification routing in runtime configuration, never in public config.""" + notifications = config.get("notifications") + if notifications is None: + return + if not isinstance(notifications, dict): + errors.append("notifications must be an object") + return + sentinel = notifications.get("quant_sentinel") + if not isinstance(sentinel, dict): + errors.append("notifications.quant_sentinel must be an object") + return + if "telegram_chat_id" in sentinel: + errors.append( + "notifications.quant_sentinel must not contain telegram_chat_id; " + "use telegram_chat_id_ref" + ) + reference = sentinel.get("telegram_chat_id_ref") + if not isinstance(reference, dict): + errors.append("notifications.quant_sentinel.telegram_chat_id_ref must be an object") + return + if reference.get("source") != QUANT_SENTINEL_CHAT_ID_SOURCE: + errors.append( + "notifications.quant_sentinel.telegram_chat_id_ref.source must be " + f"{QUANT_SENTINEL_CHAT_ID_SOURCE!r}" + ) + if reference.get("preferred_env") != QUANT_SENTINEL_CHAT_ID_PREFERRED_ENV: + errors.append( + "notifications.quant_sentinel.telegram_chat_id_ref.preferred_env must be " + f"{QUANT_SENTINEL_CHAT_ID_PREFERRED_ENV!r}" + ) + if reference.get("fallback_envs") != list(QUANT_SENTINEL_CHAT_ID_FALLBACK_ENVS): + errors.append( + "notifications.quant_sentinel.telegram_chat_id_ref.fallback_envs must be " + f"{list(QUANT_SENTINEL_CHAT_ID_FALLBACK_ENVS)!r}" + ) + aliases = sentinel.get("env_aliases") + if not isinstance(aliases, dict) or aliases.get("chat_id") != [ + QUANT_SENTINEL_CHAT_ID_PREFERRED_ENV, + *QUANT_SENTINEL_CHAT_ID_FALLBACK_ENVS, + ]: + errors.append( + "notifications.quant_sentinel.env_aliases.chat_id must match " + "telegram_chat_id_ref" + ) + + def validate_runtime_authority_status(config: dict, errors: list[str]) -> None: """Keep legacy execution metadata distinct from P0--P6 runtime authority.""" meta = config.get("meta") diff --git a/python/tests/test_runtime_settings.py b/python/tests/test_runtime_settings.py index 2dc2d7f..0dd64c0 100644 --- a/python/tests/test_runtime_settings.py +++ b/python/tests/test_runtime_settings.py @@ -167,6 +167,42 @@ def test_runtime_authority_status_does_not_grant_p0_p6_runtime_authority(self): build_config.validate(invalid), ) + def test_quant_sentinel_notification_route_is_runtime_reference_only(self): + config = build_config.load_config() + sentinel = config["notifications"]["quant_sentinel"] + + self.assertNotIn("telegram_chat_id", sentinel) + self.assertEqual( + sentinel["telegram_chat_id_ref"], + { + "source": "runtime_environment", + "preferred_env": "QSL_GLOBAL_TELEGRAM_CHAT_ID", + "fallback_envs": [ + "GLOBAL_TELEGRAM_CHAT_ID", + "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS", + ], + }, + ) + self.assertEqual( + sentinel["env_aliases"]["chat_id"], + [ + "QSL_GLOBAL_TELEGRAM_CHAT_ID", + "GLOBAL_TELEGRAM_CHAT_ID", + "STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS", + ], + ) + self.assertEqual(build_config.validate(config), []) + + def test_quant_sentinel_notification_route_rejects_public_literal(self): + config = build_config.load_config() + config["notifications"]["quant_sentinel"]["telegram_chat_id"] = "test-chat-id" + + self.assertIn( + "notifications.quant_sentinel must not contain telegram_chat_id; " + "use telegram_chat_id_ref", + build_config.validate(config), + ) + def test_manual_switch_rejects_unsupported_sync_before_variable_write(self): workflow = (ROOT / ".github" / "workflows" / "manual-strategy-switch.yml").read_text(encoding="utf-8")