feat(openclaw): modularize scripts, implement crons-as-code and EaC governance

This commit is contained in:
Moritz Graf
2026-10-16 21:25:33 +02:00
parent f4d8210cc4
commit ed3529a9d7
16 changed files with 1924 additions and 1 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
sync_crons.py
Idempotently reconciles declarative OpenClaw automations defined in crons.yaml
with the running OpenClaw gateway state.
Can run directly inside the container or remotely via kubectl exec.
"""
import json
import os
import subprocess
import sys
import yaml
def find_crons_file():
candidates = [
os.environ.get("CRONS_FILE"),
"crons.yaml",
"k8s/openclaw/crons.yaml",
"/home/node/.openclaw/scripts/crons.yaml",
os.path.join(os.path.dirname(__file__), "../crons.yaml"),
os.path.join(os.path.dirname(__file__), "crons.yaml"),
]
for c in candidates:
if c and os.path.exists(c):
return os.path.abspath(c)
raise FileNotFoundError("Could not find crons.yaml. Pass via CRONS_FILE environment variable.")
def is_inside_container():
return os.path.exists("/home/node/.openclaw") or os.environ.get("OPENCLAW_HOME") is not None
def run_openclaw_cmd(args):
if is_inside_container():
cmd = ["openclaw"] + args
else:
namespace = os.environ.get("NAMESPACE", "openclaw")
pod = os.environ.get("POD_NAME", "openclaw-0")
container = os.environ.get("CONTAINER_NAME", "openclaw")
cmd = ["kubectl", "exec", "-n", namespace, pod, "-c", container, "--", "openclaw"] + args
res = subprocess.run(cmd, capture_output=True, text=True)
if res.returncode != 0:
raise RuntimeError(f"OpenClaw CLI command failed ({' '.join(cmd)}):\n{res.stderr}\n{res.stdout}")
return res.stdout
def list_existing_jobs():
raw = run_openclaw_cmd(["cron", "list", "--json"])
try:
data = json.loads(raw)
return data.get("jobs", [])
except Exception as e:
print(f"Warning: could not parse cron list json: {e}\nRaw output: {raw[:300]}", file=sys.stderr)
return []
def reconcile_jobs():
file_path = find_crons_file()
print(f"=== Syncing OpenClaw Crons-as-Code from {file_path} ===")
with open(file_path, "r", encoding="utf-8") as f:
spec = yaml.safe_load(f)
declarations = spec.get("jobs", [])
existing_jobs = list_existing_jobs()
print(f"Found {len(declarations)} declared jobs in YAML, {len(existing_jobs)} active jobs on gateway.")
for job in declarations:
name = job.get("name")
decl_key = job.get("declarationKey") or name
desc = job.get("description", "")
sched = job.get("schedule", {})
# Match existing job by declarationKey or exact name
match = next((j for j in existing_jobs if j.get("declarationKey") == decl_key or j.get("name") == name), None)
if match:
existing_id = match.get("id")
print(f"Removing existing job '{name}' (ID {existing_id}) for idempotent recreation...")
try:
run_openclaw_cmd(["cron", "rm", existing_id])
except Exception as e:
print(f"Warning removing {existing_id}: {e}")
# Build add command
cmd = [
"cron", "add",
"--name", name,
"--declaration-key", decl_key,
"--description", desc,
]
if sched.get("kind") == "every":
cmd.extend(["--every", sched.get("every", "15m")])
elif sched.get("kind") == "cron":
cmd.extend(["--cron", sched.get("expr", "* * * * *")])
if sched.get("tz"):
cmd.extend(["--tz", sched.get("tz")])
if job.get("session"):
cmd.extend(["--session", job.get("session")])
if job.get("delivery") == "none":
cmd.append("--no-deliver")
if job.get("command"):
cmd.extend(["--command", job.get("command")])
if job.get("agent"):
cmd.extend(["--agent", job.get("agent")])
if job.get("model"):
cmd.extend(["--model", job.get("model")])
if job.get("channel"):
cmd.extend(["--channel", job.get("channel")])
if job.get("to"):
cmd.extend(["--to", job.get("to")])
if job.get("threadId"):
cmd.extend(["--thread-id", str(job.get("threadId"))])
if job.get("announce"):
cmd.append("--announce")
if job.get("message"):
cmd.extend(["--message", job.get("message")])
print(f"Registering job '{name}'...")
run_openclaw_cmd(cmd)
print(f"Successfully registered '{name}'.")
print("=== OpenClaw Crons-as-Code reconciliation complete! ===")
if __name__ == "__main__":
reconcile_jobs()