chore: protect external runtime connection settings
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Failing after 16s
Validators (Pushes and Pull Requests) / validate-core (push) Failing after 54s

This commit is contained in:
2026-07-12 12:19:34 +09:00
parent a7f9b27a55
commit 606664404b
6 changed files with 58 additions and 14 deletions
+1 -8
View File
@@ -205,14 +205,7 @@ jobs:
exit 1
fi
echo "✓ DLL verified"
# Production connection settings must come from the service
# EnvironmentFile, never from a versioned appsettings file.
SERVICE_ENV=$(sudo systemctl show "$SERVICE_NAME" -p EnvironmentFiles --value 2>/dev/null || true)
if [ -z "$SERVICE_ENV" ]; then
echo "ERROR: $SERVICE_NAME has no EnvironmentFile; database credentials are not configured"
exit 1
fi
echo "✓ External service environment configured"
echo "✓ Runtime configuration is managed outside the release artifact"
# 3. Update Symlink
echo ""
+6
View File
@@ -1,5 +1,11 @@
# 은퇴자산포트폴리오 투자 에이전트 운영 지침
## QuantEngine 운영 설정 권위
- `ConnectionStrings__DefaultConnection`은 운영 설정에서 관리한다.
- 저장소 코드, DbUp migration, CI artifact는 운영 계정 비밀번호를 생성하거나 덮어쓰지 않는다.
- 배포/검증 하네스는 설정값을 읽기만 하며, 값 자체를 로그·증빙·커밋에 기록하지 않는다.
- 설정 변경은 애플리케이션 배포와 분리된 운영 설정 변경으로 취급한다.
## 0. 최우선 원칙
- 이 파일은 운영 인덱스다. 상세 규칙은 `governance/rules/*.yaml``spec/*.yaml`를 우선한다.
- 가격, 수량, TP/SL, 점수는 오직 `spec/13_formula_registry.yaml`와 하네스 산출값만 사용한다.
+1
View File
@@ -59,6 +59,7 @@
"validate:normalized-learning-store": "python tools/validate_normalized_learning_store_v1.py",
"validate:dotnet-cutover": "python tools/validate_dotnet_postgresql_json_cutover_v1.py",
"validate:schema-model": "python tools/generate_schema_model_generation_evidence_v1.py && python tools/validate_schema_model_generation_v1.py",
"validate:runtime-settings": "python tools/validate_runtime_connection_settings_immutability_v1.py",
"test:e2e": "playwright test --project=chromium",
"test:evidence": "playwright test --project=evidence"
},
+12
View File
@@ -0,0 +1,12 @@
formula_id: RUNTIME_CONNECTION_SETTINGS_IMMUTABILITY_V1
version: 1
authority: AGENTS.md
setting_key: ConnectionStrings__DefaultConnection
owner: operations
policy:
source: runtime_environment_or_external_settings
application_may_read: true
application_may_write: false
secret_value_in_repository: forbidden
evidence: Temp/runtime_connection_settings_immutability_v1.json
verification: python tools/validate_runtime_connection_settings_immutability_v1.py
-6
View File
@@ -81,12 +81,6 @@ try
// PostgreSQL Dapper Setup
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' is required.");
if (builder.Environment.IsProduction()
&& string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ConnectionStrings__DefaultConnection")))
{
throw new InvalidOperationException(
"Production requires ConnectionStrings__DefaultConnection from the service environment.");
}
var configuredDatabase = new NpgsqlConnectionStringBuilder(connectionString).Database;
if (!string.Equals(configuredDatabase, "quantenginedb", StringComparison.OrdinalIgnoreCase))
@@ -0,0 +1,38 @@
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FILES = [
ROOT / "src/dotnet/QuantEngine.Web/Program.cs",
ROOT / ".gitea/workflows/deploy-prod.yml",
ROOT / "src/dotnet/QuantEngine.Web/appsettings.json",
ROOT / "src/dotnet/QuantEngine.Web/appsettings.Development.json",
]
REPORT = ROOT / "Temp/runtime_connection_settings_immutability_v1.json"
def main() -> int:
violations: list[str] = []
for path in FILES:
text = path.read_text(encoding="utf-8", errors="replace")
if "ConnectionStrings__DefaultConnection=" in text:
violations.append(f"embedded_connection_string:{path.relative_to(ROOT)}")
if "Environment.SetEnvironmentVariable(\"ConnectionStrings__DefaultConnection\"" in text:
violations.append(f"runtime_write:{path.relative_to(ROOT)}")
payload = {
"formula_id": "RUNTIME_CONNECTION_SETTINGS_IMMUTABILITY_V1",
"gate": "PASS" if not violations else "FAIL",
"setting_key": "ConnectionStrings__DefaultConnection",
"source": "external_runtime_settings",
"violations": violations,
}
REPORT.parent.mkdir(parents=True, exist_ok=True)
REPORT.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0 if payload["gate"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())