Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions docs/notifications-quant-sentinel.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
| 变量 | 说明 |
|------|------|
| `TELEGRAM_TOKEN` | bot token(Cloud Run 由 secret ref 注入;VPS 由 `load_telegram_env.sh`) |
| `GLOBAL_TELEGRAM_CHAT_ID` | `<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

Expand Down
8 changes: 6 additions & 2 deletions platform-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"]
}
Expand Down
55 changes: 55 additions & 0 deletions python/scripts/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
36 changes: 36 additions & 0 deletions python/tests/test_runtime_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down