188 lines
5.9 KiB
Python
188 lines
5.9 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Publish sanitized BS-SH-005 recovery watermarks to two allowlisted repos."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import pathlib
|
||
|
|
import subprocess
|
||
|
|
import urllib.request
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
|
||
|
|
|
||
|
|
ALLOWED_TARGETS = {
|
||
|
|
"REPO-014": (
|
||
|
|
"https://guanghulab.com/code/bingshuo/hololake-system-architecture.git",
|
||
|
|
"operations/BS-SH-005/persona-history/CURRENT.json",
|
||
|
|
),
|
||
|
|
"REPO-012": (
|
||
|
|
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
|
||
|
|
"eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/"
|
||
|
|
"server-watermarks/BS-SH-005-PERSONA-HISTORY-CURRENT.json",
|
||
|
|
),
|
||
|
|
}
|
||
|
|
FORBIDDEN_PUBLIC_FIELDS = {
|
||
|
|
"private_locator",
|
||
|
|
"email",
|
||
|
|
"password",
|
||
|
|
"secret",
|
||
|
|
"token",
|
||
|
|
"api_key",
|
||
|
|
"private_key",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def now_iso() -> str:
|
||
|
|
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
||
|
|
|
||
|
|
|
||
|
|
def validate_config(config: dict) -> None:
|
||
|
|
if config.get("node_id") != "BS-SH-005":
|
||
|
|
raise ValueError("publisher node is not BS-SH-005")
|
||
|
|
repositories = config.get("repositories", [])
|
||
|
|
if {item.get("id") for item in repositories} != set(ALLOWED_TARGETS):
|
||
|
|
raise ValueError("publisher repository allowlist mismatch")
|
||
|
|
for item in repositories:
|
||
|
|
if (item.get("url"), item.get("snapshot_path")) != ALLOWED_TARGETS[item["id"]]:
|
||
|
|
raise ValueError(f"publisher target mismatch for {item['id']}")
|
||
|
|
|
||
|
|
|
||
|
|
def validate_public_snapshot(snapshot: dict) -> None:
|
||
|
|
encoded = json.dumps(snapshot, ensure_ascii=False).lower()
|
||
|
|
for field in FORBIDDEN_PUBLIC_FIELDS:
|
||
|
|
if f'"{field.lower()}"' in encoded:
|
||
|
|
raise ValueError(f"private field rejected: {field}")
|
||
|
|
if snapshot.get("node_id") != "BS-SH-005":
|
||
|
|
raise ValueError("snapshot node mismatch")
|
||
|
|
if snapshot.get("persona_state") not in {"NOT_BORN", "BIRTH_GATE_PENDING"}:
|
||
|
|
raise ValueError("unexpected persona state")
|
||
|
|
|
||
|
|
|
||
|
|
def material_snapshot(snapshot: dict) -> dict:
|
||
|
|
"""Drop heartbeat-only timestamps so idle cycles never create Git commits."""
|
||
|
|
normalized = json.loads(json.dumps(snapshot))
|
||
|
|
normalized.pop("updated_at", None)
|
||
|
|
for source in normalized.get("sources", {}).values():
|
||
|
|
source.pop("updated_at", None)
|
||
|
|
return normalized
|
||
|
|
|
||
|
|
|
||
|
|
def git_env(config: dict) -> dict[str, str]:
|
||
|
|
env = dict(os.environ)
|
||
|
|
env.update(
|
||
|
|
{
|
||
|
|
"HOME": config["state_root"],
|
||
|
|
"GIT_TERMINAL_PROMPT": "0",
|
||
|
|
"GIT_CONFIG_COUNT": "1",
|
||
|
|
"GIT_CONFIG_KEY_0": "credential.helper",
|
||
|
|
"GIT_CONFIG_VALUE_0": config["credential_helper"],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return env
|
||
|
|
|
||
|
|
|
||
|
|
def run_git(env: dict[str, str], *args: str, cwd: pathlib.Path | None = None) -> None:
|
||
|
|
subprocess.run(
|
||
|
|
["git", *args],
|
||
|
|
cwd=cwd,
|
||
|
|
env=env,
|
||
|
|
check=True,
|
||
|
|
stdin=subprocess.DEVNULL,
|
||
|
|
stdout=subprocess.DEVNULL,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
text=True,
|
||
|
|
timeout=300,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def atomic_json(path: pathlib.Path, value: dict) -> None:
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
pending = path.with_name(f".{path.name}.{os.getpid()}.pending")
|
||
|
|
pending.write_text(
|
||
|
|
json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
os.replace(pending, path)
|
||
|
|
|
||
|
|
|
||
|
|
def publish_repository(
|
||
|
|
config: dict, repository: dict, snapshot: dict, env: dict[str, str]
|
||
|
|
) -> None:
|
||
|
|
checkout = pathlib.Path(config["state_root"]) / "repositories" / repository["id"]
|
||
|
|
if not (checkout / ".git").is_dir():
|
||
|
|
checkout.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
run_git(
|
||
|
|
env,
|
||
|
|
"clone",
|
||
|
|
"--branch",
|
||
|
|
"main",
|
||
|
|
"--single-branch",
|
||
|
|
repository["url"],
|
||
|
|
str(checkout),
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
run_git(env, "fetch", "origin", "main", cwd=checkout)
|
||
|
|
run_git(env, "merge", "--ff-only", "origin/main", cwd=checkout)
|
||
|
|
|
||
|
|
target = checkout / repository["snapshot_path"]
|
||
|
|
published = material_snapshot(snapshot)
|
||
|
|
if target.is_file():
|
||
|
|
current = json.loads(target.read_text(encoding="utf-8"))
|
||
|
|
current.pop("publisher", None)
|
||
|
|
if current == published:
|
||
|
|
return
|
||
|
|
published["publisher"] = {
|
||
|
|
"schema": "guanghu.persona-history-publication/v1",
|
||
|
|
"node_id": "BS-SH-005",
|
||
|
|
"repository_id": repository["id"],
|
||
|
|
"published_at": now_iso(),
|
||
|
|
"source_observed_at": snapshot.get("updated_at"),
|
||
|
|
"content_policy": "SANITIZED_PUBLIC_WATERMARK_ONLY",
|
||
|
|
}
|
||
|
|
atomic_json(target, published)
|
||
|
|
run_git(env, "add", "--", repository["snapshot_path"], cwd=checkout)
|
||
|
|
changed = subprocess.run(
|
||
|
|
["git", "diff", "--cached", "--quiet", "--exit-code"],
|
||
|
|
cwd=checkout,
|
||
|
|
env=env,
|
||
|
|
check=False,
|
||
|
|
stdout=subprocess.DEVNULL,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
text=True,
|
||
|
|
).returncode
|
||
|
|
if changed == 0:
|
||
|
|
return
|
||
|
|
if changed != 1:
|
||
|
|
raise RuntimeError("git staged diff check failed")
|
||
|
|
run_git(
|
||
|
|
env,
|
||
|
|
"-c",
|
||
|
|
"user.name=BS-SH-005 Persona History",
|
||
|
|
"-c",
|
||
|
|
"user.email=bs-sh-005@noreply.guanghulab.com",
|
||
|
|
"commit",
|
||
|
|
"-m",
|
||
|
|
"chore(history): update BS-SH-005 recovery watermark",
|
||
|
|
cwd=checkout,
|
||
|
|
)
|
||
|
|
run_git(env, "push", "origin", "HEAD:main", cwd=checkout)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--config", required=True, type=pathlib.Path)
|
||
|
|
args = parser.parse_args()
|
||
|
|
config = json.loads(args.config.read_text(encoding="utf-8"))
|
||
|
|
validate_config(config)
|
||
|
|
with urllib.request.urlopen(config["snapshot_url"], timeout=15) as response:
|
||
|
|
snapshot = json.loads(response.read())
|
||
|
|
validate_public_snapshot(snapshot)
|
||
|
|
env = git_env(config)
|
||
|
|
for repository in config["repositories"]:
|
||
|
|
publish_repository(config, repository, snapshot, env)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|