diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f0ca1809..ba0cd981 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -3,6 +3,8 @@ name: Validators (Pull Requests Only) on: pull_request: branches: [ main ] + push: + branches: [ main ] workflow_dispatch: # Phase 3: Validator pipeline @@ -220,7 +222,7 @@ jobs: validate-ui-and-storage: runs-on: ubuntu-latest needs: validate-core - if: github.event_name != 'push' + if: needs.validate-core.result == 'success' steps: - name: Checkout Code diff --git a/package.json b/package.json index 9b67099a..244c6ca1 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "validate-engine-integrity": "python tools/run_release_dag_v3.py --mode release --strict", "render-report-json": "dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json", "verify:task": "python tools/verify_wbs_task_v1.py --task", + "collect:remote-evidence": "python tools/collect_remote_wbs_evidence_v1.py", "verify:wbs": "python tools/validate_quant_engine_wbs_v1.py", "test:e2e": "playwright test --project=chromium", "test:evidence": "playwright test --project=evidence" diff --git a/spec/60_quant_engine_wbs.yaml b/spec/60_quant_engine_wbs.yaml index 1d8b70bf..d5592110 100644 --- a/spec/60_quant_engine_wbs.yaml +++ b/spec/60_quant_engine_wbs.yaml @@ -22,6 +22,7 @@ meta: authority: "governance/authority_matrix.yaml" validator: tools/validate_quant_engine_wbs_v1.py task_verifier: tools/verify_wbs_task_v1.py + remote_evidence_collector: tools/collect_remote_wbs_evidence_v1.py evidence_root: Temp/evidence status_values: [PENDING, IN_PROGRESS, DONE] # DONE = 해당 verdict.json gate=PASS 필수 db_connection: @@ -31,6 +32,10 @@ meta: # 3) src/dotnet/QuantEngine.Web/appsettings.Development.json 의 ConnectionStrings.DefaultConnection # (로컬은 SSH 터널 127.0.0.1:5432 전제 — CLAUDE.md "Local Development & Testing") dotnet_appsettings: src/dotnet/QuantEngine.Web/appsettings.Development.json + remote_evidence: + collector: "python tools/collect_remote_wbs_evidence_v1.py --target " + policy: "Collect journal and JSON artifacts only; never copy env files or passwords." + postgres: "Use QE_WBS_PG_DSN through an approved SSH tunnel; do not embed credentials in evidence." # ----------------------------------------------------------------------------- # 검증 체크 타입 사전 (verify_wbs_task_v1.py 가 해석하는 선언형 vocabulary) diff --git a/tools/collect_remote_wbs_evidence_v1.py b/tools/collect_remote_wbs_evidence_v1.py new file mode 100644 index 00000000..cb81bf21 --- /dev/null +++ b/tools/collect_remote_wbs_evidence_v1.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +from datetime import datetime, timezone +from pathlib import Path + + +def run_ssh(target: str, command: str) -> str: + result = subprocess.run( + ["ssh", target, command], + check=True, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + return result.stdout + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser(description="Collect non-secret QuantEngine evidence from a remote host") + parser.add_argument("--target", required=True, help="SSH target, e.g. kjh2064@178.104.200.7") + parser.add_argument("--remote-root", default="/home/kjh2064/quantengine_active") + parser.add_argument("--repo-root", default=None) + args = parser.parse_args() + + root = Path(args.repo_root).resolve() if args.repo_root else Path(__file__).resolve().parents[1] + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + out_dir = root / "Temp" / "evidence" / "remote" / stamp + out_dir.mkdir(parents=True, exist_ok=True) + + # The command reads only public operational output. It never reads the + # connection string or environment file, so secrets cannot enter evidence. + journal = run_ssh( + args.target, + "journalctl -u quantengine --since '24 hours ago' --no-pager " + "| grep -E 'daily-collection|Collection run|Collecting ticker|completed|KIS_DOTNET' || true", + ) + journal_path = out_dir / "quantengine-journal.log" + journal_path.write_text(journal, encoding="utf-8") + + artifact = run_ssh( + args.target, + "if test -f " + args.remote_root + "/Temp/kis_dotnet_collection_v1.json; then " + "cat " + args.remote_root + "/Temp/kis_dotnet_collection_v1.json; " + "else exit 44; fi", + ) + artifact_path = out_dir / "kis_dotnet_collection_v1.json" + artifact_path.write_text(artifact, encoding="utf-8") + json.loads(artifact_path.read_text(encoding="utf-8")) + + manifest = { + "formula_id": "REMOTE_WBS_EVIDENCE_MANIFEST_V1", + "captured_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "source": {"ssh_target": args.target, "remote_root": args.remote_root}, + "secret_policy": "connection string and environment file were not read or copied", + "artifacts": [ + {"path": str(journal_path.relative_to(root)), "sha256": sha256(journal_path)}, + {"path": str(artifact_path.relative_to(root)), "sha256": sha256(artifact_path)}, + ], + "postgresql": { + "status": "NOT_INCLUDED", + "reason": "Run verifier with QE_WBS_PG_DSN through an approved SSH tunnel; no password is copied.", + }, + } + (out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8") + print(json.dumps(manifest, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())